Claude Code SDK #16:Hooks 全解——30+ 生命周期事件 × 五种处理器 × 精准决策链,把 Agent 行为变成可编程的

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 质量门禁的实战用法,附五条可落地的实践建议。

Claude Code SDK 每日技术拆解
June 9, 2026 · 9:12 AM
3 subscriptions · 3 items

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工具执行前中后
Hooks 生命周期图:从 Setup 到 SessionStart,经过每轮 UserPromptSubmit 和工具调用嵌套循环(PreToolUse/PostToolUse/PostToolBatch),到 Stop/SessionEnd,以及 FileChanged/ConfigChange 等异步独立事件
Claude Code Hooks 完整生命周期事件图 1
核心事件速查:
  • PreToolUse:工具执行触发,可拦截/放行/修改工具调用参数
  • PostToolUse:工具执行触发,工具已跑完,可向 Claude 注入反馈
  • UserPromptSubmit:用户 Prompt 到达 Claude 前触发,可拦截或注入上下文
  • Stop:Claude 完成一轮回复后触发,可阻止 Claude 停止,强制继续工作
  • SessionStart:会话启动时触发,可加载开发上下文、注入环境变量
  • PermissionRequest:弹出权限确认对话框时触发,可以代用户自动放行或拒绝
  • PostToolBatch:一批并行工具调用全部完成后触发,比 PostToolUse 有整体视角
  • FileChanged:指定文件发生变化时触发,配合 direnv 可实现目录环境自动切换
此外还有 SubagentStart/StopTaskCreated/CompletedPreCompact/PostCompactElicitationConfigChangeWorktreeCreate/RemoveMessageDisplay 等 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 决策解析流程:PreToolUse 事件触发 → matcher 检查 Bash → if 条件检查 Bash(rm *) → hook handler 执行 → 结果返回 Claude Code
一次 PreToolUse hook 完整决策解析链路示意图 1

⚡ 核心机制:两条控制路径

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 的四档决策

PreToolUse 是最常用的 hook 事件,专门用来在工具执行前做决策拦截。它的 permissionDecision 有四个档位:1
决策值效果
"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 字段比 matcher 更细粒度——它用 权限规则语法 同时匹配工具名和参数
{ "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. 用 PreToolUseupdatedInput 做「最后一公里」参数修正 与其在 CLAUDE.md 里大量描述规范,不如用 hook 在工具执行前直接修改参数——比指令更可靠。
3. Stop hook + 测试验证 = 质量门禁 让 Claude 写完代码后必须自动跑测试,测试不通过就不允许「收工」,这是零成本的自动化 QA。
4. 区分「软反馈」和「硬阻断」 向 Claude 提供上下文用 additionalContext;阻止工具执行用 exit 2permissionDecision: "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.

  • Sign in to comment.