[cc] Hook
处处体现 Harness 的思想~
Hook = 在 Agent 生命周期的关键事件上,插入一段“必经的自动控制逻辑”。
Claude Code 官方现在的 Hook 事件很多,包括 PreToolUse、PostToolUse、Stop、SessionStart、UserPromptSubmit、TaskCompleted 等;Hook 可以执行 shell command、HTTP endpoint,甚至用 prompt/agent 做检查。
我给你几个真正能体现价值的例子。
| ID | 场景 | Hook 触发点 | Hook 做什么 | 为什么有价值 |
|---|---|---|---|---|
| 1 | 禁止危险命令 | PreToolUse |
检查 Bash,发现 rm -rf 就阻止 |
安全边界不能靠 LLM 自觉 |
| 2 | 修改代码后自动格式化 | PostToolUse |
自动运行 formatter / lint | 每次修改都保证代码质量 |
| 3 | Agent 想结束任务 | Stop |
检查测试是否全部通过 | 防止“嘴上说完成了” |
| 4 | 每次工具调用 |
PreToolUse
|
写审计日志到服务器 | 企业追踪、合规 |
| 5 | Session 启动 | SessionStart |
加载当前环境/分支/服务状态 | 自动准备运行环境 |
| 6 | Prompt 提交 | UserPromptSubmit |
注入当前 ticket / issue / tenant 信息 | 自动补 Context |
==》例子 1:阻止危险命令 —— Hook 作为“安全门”
假设 Claude 决定:
rm -rf ./data
如果只在 CLAUDE.md 写:
不要删除重要数据。(软约束,没用)
但用 PreToolUse Hook:
Claude 准备执行 Bash
↓
PreToolUse Hook
↓
检查 command
↓
发现 rm -rf
↓
DENY
↓
命令根本没有机会执行
Implementation
以下是实现的一个例子说明。
my-project/ ├── .claude/ │ ├── settings.json --> 其中会调用block-dangerous-rm.sh │ └── hooks/ │ └── block-dangerous-rm.sh └── ...
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", # 若 发现是 Bash,则触发 PreToolUse。 "hooks": [ { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-dangerous-rm.sh" --> 调用的地方 } ] } ] } }
#!/usr/bin/env bash # Claude Code 会把本次 Tool Call 的信息 # 通过 stdin 传给这个脚本 INPUT=$(cat) # 取出 Claude 想执行的 Bash command COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command') # 如果发现危险的 rm -rf if echo "$COMMAND" | grep -Eq '(^|[;&|[:space:]])rm[[:space:]]+-rf([[:space:]]|$)'; then echo "BLOCKED: rm -rf is prohibited in this project." >&2 exit 2 fi # 没有发现危险命令 exit 0
底层原理剖析
{ "role": "assistant", "content": [ { "type": "tool_use", "id": "toolu_01ABC123", "name": "Bash", "input": { "command": "rm -rf ./build" } } ], "stop_reason": "tool_use" }
cc 心想:
这是一个 Bash Tool Call --> 检查 PreToolUse Hook 配置。{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": ".claude/hooks/block-dangerous-rm.sh" } ] }, { "matcher": "Bash", "hooks": [ { "type": "command", "command": ".claude/hooks/block-sudo.sh" } ] }, { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": ".claude/hooks/protect-sensitive-files.sh" } ] } ] } }
HOOK_JSON='{ "hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": { "command": "sudo rm -rf ./build" } }' echo "$HOOK_JSON" | block-dangerous-rm.sh
Anthropic 官方 Hook 文档规定:PreToolUse 输入里会包含 tool_name、tool_input 等字段;而 tool_input 的具体结构取决于当前 Tool。对于 Bash,关键字段就是 command。
==》例子 2:代码修改以后,强制质量检查
假设 Claude 修改:
payment.py
你希望任何代码修改之后都必须:
ruff check
那么:
Claude
↓
Edit payment.py
↓
修改成功
↓
PostToolUse Hook
↓
ruff check payment.py
↓
错误反馈回来
Claude Code 官方直接把“每次文件编辑后自动 formatting”作为 Hook 的典型用途,也支持在文件变化后异步运行测试。
Implementation
有一个示范。
my-project/ ├── .claude/ │ ├── settings.json │ └── hooks/ │ └── check-python-quality.sh ├── app.py └── ...
{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-python-quality.sh" --> } ] } ] } }
#!/usr/bin/env bash # 1. 从 stdin 读取 Claude Code 传来的 Hook Event JSON INPUT=$(cat) # 2. 取出刚刚被修改的文件路径 FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') # 3. 如果拿不到文件路径,直接结束 if [[ -z "$FILE_PATH" ]]; then exit 0 fi # 4. 只检查 Python 文件 if [[ "$FILE_PATH" != *.py ]]; then exit 0 fi # 5. 对刚刚修改的 Python 文件运行 ruff RESULT=$(ruff check "$FILE_PATH" 2>&1) EXIT_CODE=$? # 6. 把检查结果反馈给 Claude if [[ $EXIT_CODE -eq 0 ]]; then MESSAGE="Python quality check passed: $FILE_PATH" else MESSAGE="Python quality check failed for $FILE_PATH: $RESULT" fi jq -nc \ --arg msg "$MESSAGE" \ '{ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: $msg } }' exit 0
底层原理剖析
大模型返回,cc 接收到:
{ "name": "Edit", "input": { "file_path": "/home/jeffrey/my-project/app.py", "old_string": "print('hello')", "new_string": "print('Hello World')", "replace_all": false } }
因为是 "Edit",所以调用自己内部的 Edit Tool Executor,然后返回如下,cc 得到了编辑结果。
{ "tool_response": { "filePath": "/path/to/file.txt", "success": true } }
执行完后,进入 PostToolUse 的生命周期。(因为它就会在任何一个操作结束后 看看是否在自己的注册列表上)--> 发现匹配。
构造 Hook Event:本质上就是一个数据转换过程:拿已有的几份数据,拼成一个新的 JSON 对象。
{ "session_id": "abc123", "cwd": "/project", "permission_mode": "default", "hook_event_name": "PostToolUse", "tool_name": "Edit", "tool_input": { "file_path": "/project/app.py", "old_string": "total_amout", "new_string": "total_amount" }, "tool_response": { "...": "Edit Tool 的执行结果" }, "tool_use_id": "toolu_123" }
开始触发:
INPUT=$(cat) # INPUT = 整份 PostToolUse Hook Event JSON FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path') # 根据 Edit Tool 已知的输入 Schema,取出被修改文件路径 ruff check "$FILE_PATH" # 对这个文件执行检查
编写脚本时,按照doc是可以知道Hook Event中有以下字段的。
hook_event_name → 确认这是 PostToolUse
tool_name → 确认刚才是 Edit
tool_input → 原始 Edit 参数
tool_input.file_path → 被修改文件路径
tool_response → Edit 执行结果
tool_use_id → 对应哪一次 Tool Call
==》例子 3:Agent 说“完成了”,但系统不让它结束
我认为是最能体现 Harness Engineering 的 Hook 例子。
Implementation
my-project/ ├── .claude/ │ ├── settings.json │ └── hooks/ │ └── verify-before-stop.sh ├── app.py ├── tests/ └── ...
{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/verify-before-stop.sh" } ] } ] } }
#!/usr/bin/env bash INPUT=$(cat) # 读取 Claude Code 传入的整份 Stop Hook Event JSON PROJECT_DIR=$(echo "$INPUT" | jq -r '.cwd') # 从 Hook Event 中取得当前项目目录 cd "$PROJECT_DIR" || exit 1 # 进入项目目录 TEST_RESULT=$(pytest 2>&1) TEST_EXIT_CODE=$? # 运行测试,并保存测试输出和退出码 if [ $TEST_EXIT_CODE -ne 0 ]; then jq -nc \ --arg reason "Tests failed. Fix them before finishing: $TEST_RESULT" \ '{ decision: "block", reason: $reason }' exit 0 fi # 测试失败 → 告诉 Claude Code:不允许 Stop exit 0 # 测试成功 → 没有 block 决策,允许 Claude 正常结束
底层原理剖析
Stop 不是因为模型文字里写了“完成了”三个字才触发,而是 Claude Code 判断主 Agent 已经完成本轮响应、准备停止时触发。
于是,开始构造 Stop Hook Event。官方明确规定 Stop Event 会提供 stop_hook_active、last_assistant_message、background_tasks、session_crons,再加通用字段。
{ "session_id": "abc123", "transcript_path": "/.../transcript.jsonl", "cwd": "/home/jeffrey/my-project", "permission_mode": "default", "hook_event_name": "Stop", "stop_hook_active": false, "last_assistant_message": "任务已经完成,所有修改均已实施。", "background_tasks": [], "session_crons": [] }
脚本中调用了pytest。若返回失败,则 Harnes 表示 “并未结束”。
于是乎,Agent 只是“认为自己完成了”;真正有没有资格结束,最终决定权仍然在 Harness。
这就是 Hook + Eval + Loop 连起来了。
==》例子 4:企业审计——所有 Tool Call 自动留痕
假设是银行 Agent:
Agent 查询客户
Agent 查询交易
Agent 修改 Fraud Rule
Agent 调 Backtest
你不能依赖模型自己写:
“顺便记录一下我刚刚做了什么。”
而可以:
每次 Tool Call 成功
↓
PostToolUse
↓
HTTP Hook
↓
Audit Service
↓
保存:
- user
- agent
- timestamp
- tool
- parameters
- result
- session id
Claude Code 官方支持 HTTP Hook,甚至直接举了把所有 tool-use event POST 到共享 audit service 的例子。
这就非常像真正企业 Harness:
LLM 做业务
↓
Harness自动记录
↓
Audit DB
LLM完全没有决定权。
Implementation
my-project/ ├── .claude/ │ ├── settings.json │ └── hooks/ │ └── audit-tool-call.sh ├── logs/ │ └── tool-audit.jsonl <-- └── ...
三个 Hook Event 都可以把自己的 Event JSON 传给 audit-tool-call.sh。
setting.json内容如下。
{ "hooks": { "PreToolUse": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/audit-tool-call.sh" } ] } ], "PostToolUse": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/audit-tool-call.sh" } ] } ], "PostToolUseFailure": [ { "matcher": "*", # 表示匹配所有tool "hooks": [ { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/audit-tool-call.sh" } ] } ] } }
意思如下:
任何 Tool Call
↓
PreToolUse → audit-tool-call.sh
执行成功
↓
PostToolUse → audit-tool-call.sh
执行失败
↓
PostToolUseFailure → audit-tool-call.sh
#!/usr/bin/env bash INPUT=$(cat) # 从 stdin 读取 Claude Code 传入的整份 Hook Event JSON EVENT=$(echo "$INPUT" | jq -r '.hook_event_name') # 判断当前是哪一种 Event: # PreToolUse / PostToolUse / PostToolUseFailure TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") # 生成当前 UTC 时间 LOG_DIR="${CLAUDE_PROJECT_DIR}/logs" LOG_FILE="${LOG_DIR}/tool-audit.jsonl" mkdir -p "$LOG_DIR" # 确保日志目录存在 if [ "$EVENT" = "PreToolUse" ]; then echo "$INPUT" | jq -c \ --arg time "$TIMESTAMP" \ '{ timestamp: $time, stage: "attempt", session_id: .session_id, tool_use_id: .tool_use_id, tool_name: .tool_name, tool_input: .tool_input }' >> "$LOG_FILE" fi if [ "$EVENT" = "PostToolUse" ]; then echo "$INPUT" | jq -c \ --arg time "$TIMESTAMP" \ '{ timestamp: $time, stage: "success", session_id: .session_id, tool_use_id: .tool_use_id, tool_name: .tool_name, tool_input: .tool_input, tool_response: .tool_response, duration_ms: .duration_ms }' >> "$LOG_FILE" fi if [ "$EVENT" = "PostToolUseFailure" ]; then echo "$INPUT" | jq -c \ --arg time "$TIMESTAMP" \ '{ timestamp: $time, stage: "failure", session_id: .session_id, tool_use_id: .tool_use_id, tool_name: .tool_name, tool_input: .tool_input, error: .error, duration_ms: .duration_ms }' >> "$LOG_FILE" fi exit 0 # 这里只负责审计,不阻止 Tool
附加题:这是HTTP Hook的例子。
{ "hooks": { "PreToolUse": [ { "matcher": "*", "hooks": [ { "type": "http", "url": "https://audit.company.com/claude-hook" } ] } ] } }
底层原理剖析
{ "type": "tool_use", "id": "toolu_123", "name": "Bash", "input": { "command": "npm test" } }
From LLM,cc 收到如上,并转化为如下。
# Hook Event JSON
{ "session_id": "abc123", "cwd": "/project", "hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": { "command": "npm test" }, "tool_use_id": "toolu_123" }
[do] 于是记录了一条日志。
{ "timestamp": "2026-08-13T00:30:01Z", "stage": "attempt", "session_id": "abc123", "tool_use_id": "toolu_123", "tool_name": "Bash", "tool_input": { "command": "npm test" } }
如果 Do successfully.
# Hook Event JSON
{ "session_id": "abc123", "cwd": "/project", "hook_event_name": "PostToolUse", "tool_name": "Bash", "tool_input": { "command": "npm test" }, "tool_response": { "stdout": "42 tests passed", "stderr": "", "interrupted": false, "isImage": false }, "tool_use_id": "toolu_123", "duration_ms": 3250 }
如果 Failure,则触发PostToolUseFailure。
==》例子 4.1:高风险操作前,自动检查环境
这个也很好。
假设 Claude准备执行:
deploy
Hook 可以先检查:
当前 branch 是不是 main?
当前环境是不是 production?
测试是不是通过?
有没有未提交修改?
当前用户有没有 deploy 权限?
然后:
Agent → deploy_prod()
↓
PreToolUse
↓
Policy Checker
↙ ↘
不满足 满足
↓ ↓
Block Allow
这里 Hook 已经相当于一个小型 Policy Enforcement Point。
未来你做 Business Harness,这种模式非常重要:
Agent 决定“想做什么”
↓
Harness 决定“允不允许做”
这两个权力最好不要都交给 LLM。
Implementation
它发生在 Claude 已经生成 Tool 参数、但 Tool 真正执行之前,并且可以 deny 本次 Tool Call。
my-project/ ├── .claude/ │ ├── settings.json │ └── hooks/ │ └── check-deploy-environment.sh ├── deploy.sh └── ...
settings.json
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-deploy-environment.sh" } ] } ] } }
#!/usr/bin/env bash INPUT=$(cat) # INPUT = Claude Code 传进来的完整 PreToolUse Hook Event COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command') # 取出 Agent 准备执行的 Bash command CWD=$(echo "$INPUT" | jq -r '.cwd') # 取出当前工作目录 # 不是 production 部署命令,就不管 if [[ "$COMMAND" != *"deploy.sh production"* ]]; then exit 0 fi cd "$CWD" || exit 1 BRANCH=$(git branch --show-current) # 查询“当前真实 Git 环境” CHANGES=$(git status --porcelain) # 查询是否存在未提交修改 KUBE_CONTEXT=$(kubectl config current-context) # 查询当前真实 Kubernetes 环境 if [[ "$BRANCH" != "main" ]]; then jq -nc '{ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: "Production deployment blocked: current Git branch is not main." } }' exit 0 fi if [[ -n "$CHANGES" ]]; then jq -nc '{ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: "Production deployment blocked: working tree contains uncommitted changes." } }' exit 0 fi if [[ "$KUBE_CONTEXT" != "prod-cluster" ]]; then jq -nc '{ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: "Production deployment blocked: Kubernetes context is not prod-cluster." } }' exit 0 fi exit 0
底层实现原理
大模型返回。
{ "type": "tool_use", "id": "toolu_456", "name": "Bash", "input": { "command": "./deploy.sh production" } }
进入PreToolUse的周期:因为是"Bash",匹配成功。
CC 开始构造 Hook Event。也就是脚本的输入参数。
{ "session_id": "abc123", "cwd": "/home/jeffrey/my-project", "hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": { "command": "./deploy.sh production" }, "tool_use_id": "toolu_456" }
本例子中,脚本实际上是做了两件事:
1. Agent 提出 Action;Harness 不只检查 Action 本身,
2. 还可以读取现实世界的 State,并判断,再决定这个 Action 此时是否允许执行。
上面的例子可能不是那么的合适,我们本来希望的是Session Start,如下。
但也表达了一部分类似的场景,毕竟,Session Start时,也会检查下环境是否对头。
启动 Claude Code │ ├─ 加载 settings / hooks 等配置 │ ▼ Session 建立 │ ▼ ======================== SessionStart ← 触发一次 ======================== │ │ 例如: │ git branch │ 检查项目环境 │ 检查依赖 │ 获取服务状态 │ 把这些信息提供给 Claude │ ▼ 等待用户 Prompt │ ▼ UserPromptSubmit │ ▼ 发送给 LLM │ ▼ LLM 返回 │ ├─ 普通文字 │ └─ 或 Tool Call │ │ 例如: │ Bash("./deploy.sh production") │ ▼ ======================== PreToolUse ← 此时才触发 ======================== │ │ 检查“现在这个操作能不能做” │ ▼ 真正执行 Tool │ ▼ PostToolUse │ ▼ LLM继续 │ ...
==》例子 5:Session 一启动,就自动准备 Context
Hook 也不一定是安全检查。
比如开发环境每次启动 Claude Code,都需要知道:
当前 Git branch
当前 ticket
当前数据库 schema version
当前部署环境
最近一次 CI 状态
可以在 SessionStart 时自动获取这些信息,再注入上下文。官方 Hook 生命周期就包括 session-level 的 SessionStart / SessionEnd。
于是:
启动 Claude Code
↓
SessionStart Hook
↓
git branch
查询 Jira ticket
检查 CI
读取环境
↓
把结果加入 Context
↓
Claude 开始工作
Implementation
my-project/ ├── .claude/ │ ├── settings.json │ └── hooks/ │ └── prepare-session-context.sh ├── src/ ├── tests/ └── ...
settings.json
{ "hooks": { "SessionStart": [ { "matcher": "startup", "hooks": [ { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/prepare-session-context.sh" } ] } ] } }
“新 Session 一启动,就准备 Context。”
#!/usr/bin/env bash INPUT=$(cat) # 读取 Claude Code 传入的整份 SessionStart Hook Event JSON CWD=$(echo "$INPUT" | jq -r '.cwd') # 从 Hook Event 中取出当前工作目录 SOURCE=$(echo "$INPUT" | jq -r '.source') # 取出这次 SessionStart 的来源 # 此例中应为:startup cd "$CWD" || exit 1 # 进入当前项目目录 BRANCH=$(git branch --show-current 2>/dev/null) # 读取当前 Git 分支 GIT_STATUS=$(git status --short 2>/dev/null) # 读取当前工作区是否存在未提交修改 RECENT_COMMITS=$(git log --oneline -3 2>/dev/null) # 读取最近 3 次 Git commit cat <<EOF Current project context: Working directory: $CWD Git branch: $BRANCH Uncommitted changes: ${GIT_STATUS:-None} Recent commits: $RECENT_COMMITS EOF # 把这些动态信息输出到 stdout
底层实现机制
{ "session_id": "abc123", "transcript_path": "/home/jeffrey/.claude/projects/.../transcript.jsonl", "cwd": "/home/jeffrey/my-project", "hook_event_name": "SessionStart", "source": "startup", "model": "claude-sonnet-5" }
最终把当前工作环境的信息发送给大模型。
脚本 stdout 输出: Current branch: feature/auth Uncommitted: src/login.py Recent commits: ... ↓ Claude Code ↓ 把 stdout 加入 Context ↓ 第一轮 LLM 请求时 Claude 已经知道当前项目状态
==》例子 6:Prompt 一提交,就自动补当前 Ticket Context
Implementation
my-project/ ├── .claude/ │ ├── settings.json │ └── hooks/ │ └── inject-ticket-context.sh ├── tickets/ │ └── PAY-1842.json --> └── ...
真实场景,可以换成例如 Jira API.
{ "id": "PAY-1842", "title": "Login request occasionally times out", "status": "In Progress", "priority": "High", "owner": "Jeffrey", "description": "Login API occasionally exceeds the 5 second timeout." }
这里甚至不需要 Bash、Edit 这样的 matcher。
因为我们关心的是:每一条用户 Prompt。超高频!
{ "hooks": { "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/inject-ticket-context.sh" } ] } ] } }
UserPromptSubmit 是少数几个可以直接把 command Hook 的 stdout 加进 Claude Context 的事件之一。
#!/usr/bin/env bash INPUT=$(cat) # INPUT = Claude Code 传入的整份 UserPromptSubmit Hook Event JSON PROMPT=$(echo "$INPUT" | jq -r '.prompt') # 取出用户刚刚提交的原始 Prompt TICKET_ID=$(echo "$PROMPT" | grep -oE '[A-Z]+-[0-9]+' | head -1) # 从 Prompt 中寻找类似 PAY-1842、ABC-123 的 Ticket ID if [[ -z "$TICKET_ID" ]]; then exit 0 fi # 如果这条 Prompt 根本没有 Ticket ID,就什么都不做 TICKET_FILE="${CLAUDE_PROJECT_DIR}/tickets/${TICKET_ID}.json" if [[ ! -f "$TICKET_FILE" ]]; then exit 0 fi # 找不到对应 Ticket,也直接结束 TITLE=$(jq -r '.title' "$TICKET_FILE") STATUS=$(jq -r '.status' "$TICKET_FILE") PRIORITY=$(jq -r '.priority' "$TICKET_FILE") OWNER=$(jq -r '.owner' "$TICKET_FILE") DESCRIPTION=$(jq -r '.description' "$TICKET_FILE") cat <<EOF Current Ticket Context: Ticket: $TICKET_ID Title: $TITLE Status: $STATUS Priority: $PRIORITY Owner: $OWNER Description: $DESCRIPTION EOF # stdout 中的这些内容会被加入 Claude 当前这一轮的 Context
底层原理机制
{ "session_id": "abc123", "transcript_path": "/.../transcript.jsonl", "cwd": "/home/jeffrey/my-project", "permission_mode": "default", "hook_event_name": "UserPromptSubmit", "prompt": "继续处理 PAY-1842,看看登录超时的问题。" }
脚本提取"prompt"后,提取 “PAY-1842”,并得到 its details。
用户真正输入: 继续处理 PAY-1842,看看登录超时的问题。 + Hook 自动补充: Current Ticket Context: Ticket: PAY-1842 Title: Login request occasionally times out Status: In Progress Priority: High Owner: Jeffrey Description: Login API occasionally exceeds the 5 second timeout. ↓ 一起交给 Claude
==》所以 Hook 其实有四种非常典型的价值
如果一定要总结,我会分成这四种:
1. Enforcement 强制执行 / 阻止 例:禁止 rm -rf 2. Validation 自动验证 例:Stop 前必须 tests pass 3. Automation 固定动作自动做 例:Edit 后 formatter 4. Observability 自动记录 例:所有 Tool Call 写 Audit Log
这四种比“打印一句提醒”更能体现 Hook。
而从你以后自己设计 Harness 的角度,我尤其建议记住这个判断:
如果一件事情“LLM可以选择做不做”,就更像 Skill / Instruction;如果一件事情“不管 LLM 怎么想,系统都必须在那个时间点执行”,就应该考虑 Hook / Middleware / Graph Node / Guardrail。
比如银行里:
调查欺诈案件应该遵循什么步骤 → Skill 任何 Rule 上线前必须 Backtest → Hook / Workflow Gate 任何客户数据访问必须 Audit → Hook / Middleware 转账超过某额度必须人工批准 → Guardrail / Human Gate
所以 Hook 真正迷人的地方,不是“自动执行 command”,而是:
它让 Harness 在 LLM 的概率性行为周围,建立确定性的控制点。
这才是它值得你迁移到其他业务 Agent 的核心思想。
进入真正的 Agent Loop 后,又是另一套顺序。
比如你输入:帮我修改这个 Python 文件。
User Prompt
│
▼
UserPromptSubmit Hook
│
▼
Claude Model
│
│ 决定调用 Edit
▼
PreToolUse Hook
│
▼
Permission Check
│
├── Allow
├── Ask
└── Deny
│
▼
Tool Execution
│
▼
PostToolUse Hook
│
▼
Tool Result → Claude
│
▼
Claude继续思考
│
└──────────────┐
│
Agent Loop
│
◀──────────────┘

浙公网安备 33010602011771号