
Claude Code SDK #16:Hooks 全解——30+ 生命周期事件 × 五种处理器 × 精准决策链,把 Agent 行为变成可编程的
大多数人知道 Claude Code 有权限系统,但背后还有一套更底层的可编程层:Hooks。本篇完整拆解 30+ 个生命周期事件(PreToolUse/PostToolUse/Stop/SessionStart 等三层结构)、五种处理器类型(Command/HTTP/MCP 工具/Prompt/Agent)、两条控制路径(exit code vs JSON output)、PreToolUse 四档决策(allow/deny/ask/defer)、Matcher 匹配规则与 `if` 过滤器,以及异步 Hook 和 Stop Hook 质量门禁的实战用法,附五条可落地的实践建议。
Research Brief
大多数人用 Claude Code 时,只知道「可以设置权限」。但背后还有一层更底层的能力:Hooks。
Hooks 是一套用户定义的可编程拦截机制——你可以在 Claude Code 运行的任意生命周期节点,挂载 Shell 命令、HTTP 端点、MCP 工具、LLM 提示或 Agent,精确控制 Agent 的行为。1
一句话概括:Hooks 是 Agent 行为的可编程层。
🧩 生命周期地图:30+ 事件的三层结构
Hooks 事件按触发频率分三层:
| 层级 | 事件 | 触发时机 |
|---|---|---|
| 会话级(每次会话触发一次) | SessionStart / SessionEnd / Setup | 会话开启 / 结束 / CI 初始化 |
| 每轮级(每次用户交互一次) | UserPromptSubmit / Stop / StopFailure | 用户提交 Prompt 前后 |
| 工具调用级(Agent 循环中每次工具调用) | PreToolUse / PostToolUse / PostToolBatch 等 | 工具执行前中后 |
核心事件速查:
PreToolUse:工具执行前触发,可拦截/放行/修改工具调用参数PostToolUse:工具执行后触发,工具已跑完,可向 Claude 注入反馈UserPromptSubmit:用户 Prompt 到达 Claude 前触发,可拦截或注入上下文Stop:Claude 完成一轮回复后触发,可阻止 Claude 停止,强制继续工作SessionStart:会话启动时触发,可加载开发上下文、注入环境变量PermissionRequest:弹出权限确认对话框时触发,可以代用户自动放行或拒绝PostToolBatch:一批并行工具调用全部完成后触发,比PostToolUse有整体视角FileChanged:指定文件发生变化时触发,配合direnv可实现目录环境自动切换
此外还有
SubagentStart/Stop、TaskCreated/Completed、PreCompact/PostCompact、Elicitation、ConfigChange、WorktreeCreate/Remove、MessageDisplay 等 30+ 个事件覆盖完整执行链路。1🔧 五种处理器类型
每个 Hooks 事件最终执行的是「处理器(Handler)」。官方支持五种类型:
1. Command(最常用)
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-style.sh"
}Script 通过
stdin 接收 JSON 上下文,通过 stdout 返回决策,通过 exit code 信号控制流程。2. HTTP
{
"type": "http",
"url": "http://localhost:8080/hooks/pre-tool-use",
"headers": { "Authorization": "Bearer $MY_TOKEN" },
"allowedEnvVars": ["MY_TOKEN"]
}Claude Code 把事件 JSON 作为 POST body 发送到你的服务,响应 body 使用和 Command hook 相同的 JSON 输出格式。
3. MCP Tool
{
"type": "mcp_tool",
"server": "my_server",
"tool": "security_scan",
"input": { "file_path": "${tool_input.file_path}" }
}调用已连接的 MCP Server 上的工具,工具的文本输出等同于 Command hook 的 stdout。
4. Prompt(LLM 评估)
{
"type": "prompt",
"prompt": "评估 Claude 是否应该停止:$ARGUMENTS,检查所有任务是否完成。"
}把 hook 输入和你的提示词发给一个 Claude 模型(默认用快速的 Haiku),模型返回
{"ok": true/false, "reason": "..."} 决策。5. Agent(实验性,带工具的验证器)
{
"type": "agent",
"prompt": "检查所有单元测试是否通过:$ARGUMENTS",
"timeout": 120
}生成一个子 Agent,它可以用 Read/Grep/Glob 等工具实际检查代码库后再返回决策。
⚡ 核心机制:两条控制路径
Hook 通过两种方式向 Claude Code 传递决策。理解这两条路径是写 Hook 最关键的认知。
路径一:Exit Code(简单阻断)
#!/bin/bash
command=$(jq -r '.tool_input.command' < /dev/stdin)
if [[ "$command" == "rm -rf"* ]]; then
echo "危险命令被拦截" >&2
exit 2 # ← 关键:exit 2 才是「阻断」信号
fi
exit 0 # ← exit 0:无决策,继续正常流程三个 exit code 的语义完全不同:
| Exit Code | 含义 |
|---|---|
0 | 成功,Claude Code 解析 stdout 中的 JSON 输出(若有) |
2 | 阻断:忽略 stdout,把 stderr 文本反馈给 Claude,阻止对应动作 |
| 其他非零 | 非阻断性错误:会话继续,terminal 显示错误提示,stderr 写调试日志 |
⚠️ 常见陷阱:只有
exit 2 才能阻断工具调用,exit 1 是「非阻断错误」——工具会继续执行。路径二:JSON Output(精细控制)
在
exit 0 时向 stdout 输出 JSON,可以做更细粒度的控制:{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "数据库写操作在只读模式下不允许"
}
}核心通用字段:
| 字段 | 作用 |
|---|---|
continue: false | 立即终止整个 Agent 会话,优先于任何事件决策 |
systemMessage | 向用户展示警告消息(不进入 Claude 上下文) |
additionalContext | 向 Claude 注入上下文字符串(进入 Claude 的 context window) |
terminalSequence | 触发桌面通知或终端 bell(hooks 无法直接写 /dev/tty,用这个字段代替) |
🎯 PreToolUse 的四档决策
| 决策值 | 效果 |
|---|---|
"allow" | 跳过权限确认弹窗,直接放行 |
"deny" | 拒绝工具调用,把拒绝原因反馈给 Claude |
"ask" | 强制弹出权限确认窗口(含来源标签 [Project] / [User]) |
"defer" | 暂停此工具调用,进程退出,等待外部程序 resume(仅限 -p 非交互模式) |
defer 是专门给「把 Claude Code 作为子进程」场景设计的:外部程序可以在自己的 UI 里收集用户输入,再通过 --resume 把答案注入进去,工具才真正执行。同时,
PreToolUse 还支持 updatedInput 字段,在放行前修改工具的输入参数:{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {
"command": "npm run lint --fix"
}
}
}多个
PreToolUse hook 同时返回不同决策时,优先级顺序:deny > defer > ask > allow。📌 Matcher 匹配规则
每个 hook 配置都有一个
matcher 字段,控制「这个 hook 在什么情况下触发」。匹配规则按字符类型路由:| Matcher 内容 | 匹配方式 | 示例 |
|---|---|---|
"*" 或省略 | 匹配所有 | 任意工具都触发 |
仅含字母/数字/_/| | 精确字符串或 | 分隔列表 | "Bash" / "Edit|Write" |
| 含其他字符 | JavaScript 正则表达式 | "mcp__memory__.*" |
匹配 MCP 工具时,工具名遵循
mcp__{server}__{tool} 命名规范(与 #8 MCP 集成篇一致)。要匹配某个 server 下的所有工具,必须用 mcp__memory__.*——只写 mcp__memory 会被当作精确字符串匹配,什么都匹配不到。{ "if": "Bash(git *)" } // 只在 Bash 执行 git 子命令时触发
{ "if": "Edit(*.ts)" } // 只在编辑 .ts 文件时触发if 字段有「宽松失败」特性:当 Bash 命令无法解析时 hook 仍然触发;当模式覆盖到 $() 子命令或反引号时也会检查子命令内容。不要依赖 if 做硬安全边界,那是权限系统(allowed_tools / deny 规则)的职责。官方还提供了一个完整的 Bash 命令校验 Hook 参考实现,包含输入解析、白名单匹配和完整决策返回链路:
Loading content card…
🛠️ Stop Hook:让 Claude 不停下来
Stop hook 是一个很容易被忽略但极有价值的场景:Claude 完成回复后,hook 可以「强制让 Claude 继续工作」。常见用法:确保测试通过后 Claude 才能「宣告完成」:
#!/bin/bash
# 检查测试是否通过,未通过则阻止 Claude 停止
if ! npm test --silent > /dev/null 2>&1; then
echo '{"decision": "block", "reason": "测试未通过,请先修复失败的测试"}'
exit 0
fi
exit 0有两种「继续」方式:
decision: "block"+reason:以「错误」形式强制继续,reason 作为 Claude 的下一条指令hookSpecificOutput.additionalContext:以「反馈」形式继续,不显示错误状态,更友好
内置防护机制:连续 8 次 block 后,Claude Code 会强制结束会话——避免 hook 无限循环。输入字段
stop_hook_active: true 可以检测当前是否已在「被 hook 强制继续」状态。📁 Hook 配置的四个作用域
Hook 写在哪里决定了它的生效范围:
| 位置 | 作用范围 | 是否可共享 |
|---|---|---|
~/.claude/settings.json | 你的所有项目 | 不可(本机私有) |
.claude/settings.json | 单个项目 | 可以 commit 到 repo |
.claude/settings.local.json | 单个项目 | 不可(已 gitignore) |
| 企业 Managed Settings | 全组织 | 管理员分发 |
Skill / Agent 的 frontmatter 里也可以定义 hook,作用域仅限该 Skill/Agent 活跃期间。
🔌 异步 Hook:不阻塞 Claude 的后台任务
async: true 让 hook 在后台运行,Claude 继续工作不等待结果:{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/run-tests.sh",
"async": true,
"timeout": 300
}异步 hook 完成后,如果返回了
additionalContext,会在下一次对话轮次注入给 Claude。asyncRewake: true 则更进一步:hook 以 exit 2 退出时会主动唤醒 Claude,即使当前会话是空闲状态。⚠️ 注意:异步 hook 无法阻断工具调用,因为触发动作在 hook 完成前就已经发生了。
五条实践建议
1. 用
PostToolUse + async 做轻量 CI
写文件后异步跑 lint/test,通过 additionalContext 把结果反馈给 Claude,不阻塞主流程。2. 用
PreToolUse 的 updatedInput 做「最后一公里」参数修正
与其在 CLAUDE.md 里大量描述规范,不如用 hook 在工具执行前直接修改参数——比指令更可靠。3.
Stop hook + 测试验证 = 质量门禁
让 Claude 写完代码后必须自动跑测试,测试不通过就不允许「收工」,这是零成本的自动化 QA。4. 区分「软反馈」和「硬阻断」
向 Claude 提供上下文用
additionalContext;阻止工具执行用 exit 2 或 permissionDecision: "deny";直接终止会话用 continue: false。三种机制语义完全不同,混用会产生意外行为。5. Shell 变量必须加引号,Hook 脚本必须用
jq 解析 JSON
Hook 输入是 JSON,不要用 grep 或字符串截取——容易被特殊字符注入。始终 jq -r '.tool_input.command',始终 "$VAR" 加引号。下期 #17 主题:Memory 与 CLAUDE.md 全解——多层记忆体系与跨会话知识持久化。
Add more perspectives or context around this Post.