第 14 章 Hooks 系统
Hooks 是把工程纪律编码成自动执行的生命周期钩子——让低风险动作自动跑、高风险动作必停。权限规则(第 05 章)负责"谁能做",Hooks 负责"做完前后会发生什么"。
14.1 结论:Hooks 是工程纪律的可执行化
第 06 章讲可执行闭环时强调:把"完成"变成机器可检查的状态。Hooks 就是把这条闭环焊到 Claude Code 生命周期上的那道焊缝——它让你在关键节点(用户提交 prompt、工具调用前后、会话结束)插入自己的逻辑,无需 Claude 同意、无需 Claude 知道。
这与让 Claude"自己跑测试"有本质区别:Claude 跑测试是模型决策,会忘、会跳过、会被 prompt 注入绕过;Hook 是 harness 层执行,模型无法绕过。第 05 章的权限规则决定"动作允不允许",第 14 章的 Hook 决定"动作前后再追加什么"。
典型落地:写完文件自动 prettier 格式化、commit 前强制跑测试、敏感文件被改时告警、Claude 停下时桌面通知。这四件事一旦写进 settings.json,就不再依赖 Claude 的"自觉"。
核对日期:2026-07-08,Claude Code v2.1.202+。官方文档 https://code.claude.com/docs/en/hooks。
14.2 五种 Hook 类型对比
Hooks 已从早期的"shell 命令"单一形态,扩展为五种类型,各有适用场景:
| 类型 | 机制 | 适用场景 | 默认超时 |
|---|---|---|---|
command | 本地 shell 命令(stdin 吃 JSON) | 格式化、lint、测试、本地脚本 | 600s |
http | POST 到 URL | 远程审计、企业网关、CI 触发 | 600s |
mcp_tool | 调 MCP 服务器上的工具 | 复用 MCP 生态(security_scan 等) | 600s |
prompt | 单轮 LLM 评估(默认 Haiku) | 语义判断("测试都跑了吗") | 30s |
agent (实验性) | 带 Read/Grep/Glob 的 subagent | 需要读文件验证的复杂条件 | 60s |
为什么从 command 扩展到后四种?因为不是所有判断都该用 shell 写:
http让企业能把所有工具调用送到中心审计服务,无需在每台机器上部署脚本;mcp_tool让你复用已经接好的 MCP 服务器(例如security_scan),不重复造轮子;prompt解决"判断需要语义理解"的场景——例如 Stop 前让 LLM 评估"所有任务真的完成了吗",shell 写不出这种判断;agent进一步,让验证者能读文件、跑 grep,例如 commit 前检查是否引入了新的console.log。
实验性标注:
agent类型官方明确标注 experimental,行为可能变。生产环境优先用command。
类型支持矩阵(重要,不是所有事件都支持所有类型):
- 全五类支持:PreToolUse、PostToolUse、PostToolUseFailure、PostToolBatch、UserPromptSubmit、UserPromptExpansion、Stop、SubagentStop、PermissionRequest、PermissionDenied、TaskCreated、TaskCompleted、TeammateIdle
- 仅 command/http/mcp_tool:ConfigChange、CwdChanged、FileChanged、Elicitation、ElicitationResult、InstructionsLoaded、Notification、PostCompact、PreCompact、SessionEnd、StopFailure、SubagentStart、WorktreeCreate、WorktreeRemove
- 仅 command/mcp_tool:SessionStart、Setup
SessionStart 不支持 http/prompt/agent,因为启动时太早,网络和 LLM 都可能没就绪。
14.3 事件全览(按生命周期分组)
官方已扩展到 30+ 事件。按触发节奏分四组:
14.3.1 会话级(一次会话触发一次或几次)
| 事件 | 触发 | 能否阻断 |
|---|---|---|
SessionStart | 会话开始或恢复 | 否,只能注入 context |
SessionEnd | 会话结束 | 否,清理用 |
Setup | 仅 --init-only / -p --init / -p --maintenance | 否 |
InstructionsLoaded | CLAUDE.md 或 .claude/rules/*.md 被加载 | 否,审计用 |
ConfigChange | 配置文件变化 | 是(但 policy_settings 不可阻断) |
CwdChanged | 工作目录变化(如 cd) | 否 |
FileChanged | 被 watch 的文件变化 | 否 |
PreCompact / PostCompact | 上下文压缩前后 | Pre 可阻断,Post 不可 |
WorktreeCreate / WorktreeRemove | worktree 创建/删除 | Create 任何非零退出即失败 |
14.3.2 每轮(每个用户 prompt 触发)
| 事件 | 触发 | 能否阻断 |
|---|---|---|
UserPromptSubmit | 用户提交 prompt,Claude 处理前 | 是,擦除 prompt |
UserPromptExpansion | /skill 类命令展开前 | 是,阻止展开 |
Stop | Claude 响应完成 | 是,强制继续 |
StopFailure | API 错误导致中断 | 否,输出被忽略 |
MessageDisplay | 助手消息流式渲染时 | 否,只改显示不改 transcript |
Notification | Claude Code 发通知(权限提示、空闲等) | 否,side effect 用 |
14.3.3 工具循环(每个工具调用触发)
| 事件 | 触发 | 能否阻断 |
|---|---|---|
PreToolUse | 工具执行前 | 是,allow/deny/ask/defer |
PermissionRequest | 权限对话框弹出前 | 是,allow/deny |
PermissionDenied | auto 模式分类器拒绝 | 否,只能 retry: true 让模型重试 |
PostToolUse | 工具成功后 | 否(工具已跑),可注入反馈 |
PostToolUseFailure | 工具失败后 | 否,可注入纠正反馈 |
PostToolBatch | 一批并行工具调用全部完成后 | 是,阻断下一轮模型调用 |
14.3.4 Subagent 与团队
| 事件 | 触发 | 能否阻断 |
|---|---|---|
SubagentStart | subagent 启动 | 否,可注入 context |
SubagentStop | subagent 完成 | 是,强制继续 |
TaskCreated / TaskCompleted | Agent Teams 任务创建/完成 | 是 |
TeammateIdle | 团队成员即将闲置 | 是,强制继续工作 |
Elicitation / ElicitationResult | MCP server 请求用户输入 | 是 |
14.3.5 重点关注四个事件
PreToolUse:用得最多。在工具执行前拦截,可以 allow/deny/ask/defer,还能改写工具参数(updatedInput)。是"敏感文件修改必停"的实现位置。
PostToolUse:工具成功后触发。是"格式化、lint、测试"的实现位置。注意它无法阻止已发生的动作——要阻止必须在 PreToolUse。
Stop:Claude 停下时触发。用 decision: "block" 让它继续工作。内置 /goal 命令本质就是会话级 prompt-based Stop hook。注意 8 次连续阻断后 Claude Code 强制结束。
UserPromptSubmit:用户提交 prompt 时触发。可以注入 context(如当前 git 分支、最近 CI 结果),也可以阻断(如检测到 prompt 里含密钥)。
14.4 配置示例:四个可直接跑的 Hook
配置位置(优先级从高到低):Managed policy > ~/.claude/settings.json > .claude/settings.json > .claude/settings.local.json > Plugin hooks/hooks.json > Skill/agent frontmatter。后定义的不覆盖前定义的,而是全部并行执行,相同 handler 自动去重。
14.4.1 示例 1:PostToolUse 自动 prettier 格式化
目标:Claude 每次 Edit/Write 文件后,自动跑 prettier 格式化该文件。
.claude/hooks/format.sh:
#!/bin/bash
# 读取 stdin 的 JSON,提取文件路径,跑 prettier
FILE_PATH=$(jq -r '.tool_input.file_path // empty')
[ -z "$FILE_PATH" ] && exit 0
# 只处理 .ts/.tsx/.js/.jsx/.json/.css
case "$FILE_PATH" in
*.ts|*.tsx|*.js|*.jsx|*.json|*.css) ;;
*) exit 0 ;;
esac
npx prettier --write "$FILE_PATH" 2>&1 || exit 0
# 通过 additionalContext 告诉 Claude 已格式化
jq -nc --arg f "$FILE_PATH" \
'{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:("Formatted " + $f)}}'.claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format.sh",
"args": []
}
]
}
]
}
}- 命令:
chmod +x .claude/hooks/format.sh,然后让 Claude 编辑任意.ts文件。 - 预期输出:终端显示
Formatted /path/to/file.ts,文件被 prettier 重排。 - 验证:编辑后立刻
git diff看到 prettier 风格的变更。 - 失败边界:prettier 不存在时
npx prettier失败,脚本exit 0静默跳过——这是故意的,避免 hook 报错阻塞工作流。若需强提示,改成exit 2让 Claude 看到 stderr。注意additionalContext上限 10000 字符,超长会被写文件并返回路径。
14.4.2 示例 2:PreToolUse 在 git commit 前强制跑测试
目标:Claude 调 git commit 时,如果测试没过就阻止。
.claude/hooks/prevent-bad-commit.sh:
#!/bin/bash
COMMAND=$(jq -r '.tool_input.command')
# 只拦截 git commit
if ! echo "$COMMAND" | grep -qE '^git commit'; then
exit 0
fi
# 跑测试
if ! npm test 2>/tmp/test-fail.log; then
jq -nc --arg msg "Tests failed, commit blocked: $(head -20 /tmp/test-fail.log)" \
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$msg}}'
else
exit 0
fi.claude/settings.json(节选):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/prevent-bad-commit.sh",
"args": []
}
]
}
]
}
}更简洁的写法是用 if 字段做初筛,避免每次 Bash 调用都启动脚本:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git commit*)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/prevent-bad-commit.sh",
"args": []
}
]
}
]
}
}if 用 permission rule 语法,只有 Bash 子命令匹配 git commit* 时脚本才启动,其他 Bash 调用零开销。
- 命令:让 Claude 跑
git commit -m "..."。 - 预期输出:测试失败时,Claude 收到
deny决策和失败日志,转去修测试;测试通过时正常 commit。 - 验证:
claude --debug-file /tmp/cc.log后查看日志,能看到 hook 执行记录。 - 失败边界:测试套件本身有副作用(写数据库)时会每次 commit 都触发——别在这种项目用。
if是 best-effort 过滤,复杂管道(如npm test && git commit)仍会触发,因为if检测到子命令匹配。硬性约束请用权限规则(第 05 章),不要靠 hook。
14.4.3 示例 3:敏感文件修改告警
目标:任何对 .env、密钥、CI 配置的修改,终端响铃并桌面通知。
.claude/hooks/alert-sensitive.sh:
#!/bin/bash
FILE_PATH=$(jq -r '.tool_input.file_path // empty')
case "$FILE_PATH" in
*.env|.env*|*secrets*|*.pem|*id_rsa*|.github/workflows/*.yml) ;;
*) exit 0 ;;
esac
# 桌面通知(OSC 777 兼容 urxvt/Ghostty/Warp/iTerm2)
SEQ=$(printf '\033]777;notify;Claude Code;Sensitive file modified: %s\007' "$FILE_PATH")
jq -nc --arg seq "$SEQ" '{terminalSequence:$seq,systemMessage:"Sensitive file modified: '"$FILE_PATH"'"}'.claude/settings.json(节选):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/alert-sensitive.sh",
"args": []
}
]
}
]
}
}- 命令:让 Claude 编辑
.env。 - 预期输出:终端响铃,桌面弹通知,会话内显示
Sensitive file modified: /path/.env。 - 验证:在 iTerm2/Ghostty/Warp 下应看到系统通知。
- 失败边界:
terminalSequence是 v2.1.141+ 的字段,之前的做法是直接写/dev/tty,但 v2.1.139 起 hook 跑在独立 session 里没有控制终端,/dev/tty失效。terminalSequence只允许 OSC 0/1/2/9/99/777 和 BEL,其他转义被忽略,所以无法用它改颜色或移动光标。注意这只告警不阻断——要阻断把脚本结尾改成permissionDecision: "ask"让用户确认。
14.4.4 示例 4:Stop 时桌面通知
目标:Claude 完成响应回到空闲状态时,通知用户回来审查。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash -c 'SEQ=$(printf \"\\033]777;notify;Claude Code;Claude is done\\007\"); jq -nc --arg seq \"$SEQ\" \"{terminalSequence:\\$seq}\"'",
"args": []
}
]
}
]
}
}- 命令:让 Claude 跑一个长任务(如重构),切到别的窗口工作。
- 预期输出:Claude 停下时桌面弹通知"Claude is done"。
- 验证:切换窗口能看到系统通知中心有记录。
- 失败边界:Stop hook 若返回
decision: "block"会强制 Claude 继续——本例没返回 decision 所以不阻塞。连续 8 次 block 后 Claude Code 强制结束,防止死循环。stop_hook_active字段为 true 时说明已是阻断后的续跑,应避免再次 block 同一条件。
14.5 PreToolUse 决策控制:allow/deny/ask/defer
PreToolUse 是唯一用 hookSpecificOutput.permissionDecision 而非顶层 decision 的事件,因为它需要比"block/不 block"更细的控制:
| 值 | 效果 | 仍会评估 deny/ask 规则? |
|---|---|---|
allow | 跳过权限提示,直接执行 | 是 |
deny | 阻止工具调用,原因给 Claude | 是 |
ask | 弹权限提示让用户确认 | 是 |
defer | 退出,Claude Code 进程退出码带 stop_reason: "tool_deferred",留给外层 SDK 接管 | 否 |
多 hook 冲突时优先级:deny > defer > ask > allow。只要有一个 deny,就 deny。
updatedInput 可以在 allow/ask 时改写工具参数。典型用法:PreToolUse 拦截 AskUserQuestion,通过自己的 UI 收集答案,然后用 updatedInput.answers 返回,跳过终端交互——这是 Agent SDK 应用接管交互的关键机制。
defer 只在非交互模式(claude -p)生效,交互模式会被忽略并打 warning。它要求本轮只有单次工具调用,批量调用时 defer 失效。
弃用提醒:PreToolUse 早期用顶层
decision: "approve"/"block",已弃用,映射到allow/deny。新代码用hookSpecificOutput.permissionDecision。其他事件(PostToolUse、Stop 等)仍用顶层decision: "block"。
14.6 适合与不适合
适合交给 Hook
- 格式化/lint:PostToolUse 上挂 prettier、eslint --fix,无需 Claude 记得
- 校验/测试:PreToolUse 上挂"commit 前跑测试""push 前跑安全扫描"
- 告警:敏感文件修改、生产环境操作、密钥外泄
- 自动注入 context:SessionStart 注入当前分支、最近 CI 结果;UserPromptSubmit 注入相关 issue
- 环境管理:CwdChanged 上跑 direnv 逻辑,FileChanged 上重载 .envrc
- 审计:ConfigChange、InstructionsLoaded 做合规日志
- 非交互场景的交互接管:Agent SDK 用
defer把 AskUserQuestion 抛回自己的 UI
不适合交给 Hook
- 复杂逻辑分支:Hook 应短、快、确定。如果你需要"如果 A 则这样,否则如果 B 则那样"的多层判断——那是 Skill 的活,让 Claude 调 skill。
- 需要 Claude 理解语义的判断:用
prompt类型 hook 而非 command 写正则。但即便如此,判断"代码是否正确"不该靠 hook,该靠/code-review和/verify。 - 生产环境操作:红线,见第 05 章。Hook 跑在你用户权限下,能删库——别在 hook 里放
rm -rf。 - 替代人类审查:Hook 是自动化的第一道闸,不是最后一道。
git diff人工审查永远是终点(第 06 章 6.2 节步骤 7)。 - 跨工具调用的状态机:Hook 之间无共享状态(每次都是新进程)。要状态就用文件或 MCP server。
14.7 失败边界
14.7.1 Hook 报错阻塞工作流
Exit 2 是阻断信号,但不同事件行为不同。PostToolUse 退出 2 只是给 Claude 看 stderr,工具已经跑过了——这时 hook 报错不会回滚已发生的写操作。要阻止写必须在 PreToolUse。
UserPromptSubmit 默认超时只有 30 秒(其他事件 600 秒),因为它每轮 prompt 都跑。超时的 hook 在 v2.1.196+ 会显示通知,之前版本静默丢弃。如果你的 hook 慢,显式设 timeout。
SessionEnd 默认 1.5 秒超时,因为会话要尽快退出。要更长就设 CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS 环境变量(最大 60 秒)。
14.7.2 异步 hook 时序问题
async: true 让 hook 后台跑,Claude 立刻继续。但 async hook 的输出在下一轮对话才送达——如果会话空闲,输出就一直等。这意味着:
- 异步测试结果不会立刻影响当前工具调用,只会在 Claude 下一轮被读到
- 会话结束时未完成的 async hook 会被丢弃
- async hook 不能返回
decision/permissionDecision——动作已经发生了
需要"后台跑完立刻叫醒 Claude"用 asyncRewake: true,它在 exit 2 时立刻唤醒 Claude 并把 stderr 作为 system reminder 注入。
14.7.3 过度依赖 hook 替代审查
这是最大的陷阱。Hook 写多了,容易产生"我有 hook 兜底,不用细看 diff"的错觉。实际上:
- Hook 是确定性规则,无法判断业务正确性
- 正则匹配有漏网(
if过滤是 best-effort,官方明确说"用权限系统而非 hook 做硬性约束") - hook 本身有 bug 时会静默失败(exit 1 是 non-blocking,Claude Code 照常进行)
- prompt/agent hook 依赖 LLM,会误判
正确姿态:hook 做机械重复工作(格式化、测试、告警),人类做语义审查(git diff + /code-review)。两者不可互替。
14.7.4 安全红线
官方明确:command hook 跑在你用户的全权限下,能改/删任何你能改/删的文件。因此:
- 永远用
${CLAUDE_PROJECT_DIR}而非拼字符串 - 永远双引号包裹变量:
"$VAR"而非$VAR - 校验路径不含
.. - 跳过
.env、.git/、密钥文件 - 测试所有 hook 命令再加进配置
- 别在 hook 里放破坏性命令(
rm -rf、git push --force)
企业可用 allowManagedHooksOnly 禁用用户/项目/插件 hook,只允许管理员批准的 managed hook。插件强制启用的 hook 可通过 enabledPlugins 分发。
14.8 与第 05 章权限、第 16 章 Skills 的分工
三者容易混淆,边界如下:
| 机制 | 层级 | 回答的问题 | 谁触发 |
|---|---|---|---|
| 权限规则(第 05 章) | harness,工具调用前 | "这个动作允许吗?" | harness 自动 |
| Hooks(第 14 章) | harness,生命周期节点 | "动作前后再做什么?" | harness 自动 |
| Skills(第 16 章) | 模型,Claude 主动调 | "这个任务怎么做?" | Claude 决策 |
关键区别:
- 权限规则是布尔门(allow/deny/ask),无副作用,无状态
- Hook 是带副作用的钩子,可以跑命令、发请求、注入 context,但模型无法直接调用
- Skill 是Claude 可调用的能力包,模型决定何时用,可以包含复杂多步逻辑
举例区分:
- "禁止
git push --force"→ 权限规则(deny) - "commit 前跑测试"→ Hook(PreToolUse on Bash
git commit) - "按团队规范写 commit message"→ Skill(
/commitbundled skill)
混淆常见错:把"格式化代码"写成 skill 让 Claude 调。后果是 Claude 忘了调就不格式化。正确做法是 PostToolUse hook,Claude 没法绕过。
反过来:把"审查 PR"写成 hook。后果是 hook 写不出审查逻辑,只能跑 lint。正确做法是 /code-review skill,让 Claude 用模型能力审查 diff。
三层防御:权限规则挡住不该做的 → Hook 自动做该做的机械动作 → Skill 让 Claude 按规范做需要语义判断的任务。三者各司其职,缺一不可。
14.9 调试与排查
/hooks:只读浏览所有已配置 hook,显示来源(User/Project/Local/Plugin/Session/Built-in)claude --debug:写日志到~/.claude/debug/<session-id>.txtclaude --debug-file /path/to/log:指定日志路径CLAUDE_CODE_DEBUG_LOG_LEVEL=verbose:看 hook matcher 计数和匹配细节disableAllHooks: true:临时关掉所有 hook(不影响 managed hook)- shell profile 启动时打印文本会污染 hook 的 JSON 解析——hook 的 stdout 必须只有 JSON
常见坑:
- Hook 不触发:matcher 写错。
Edit|Write是精确匹配,Edit.*是正则。mcp__memory不会匹配任何工具(精确匹配路径),要mcp__memory__.* - JSON 解析失败:hook 脚本打印了非 JSON 文本(如 shell 启动 banner)。用
jq -nc构造 JSON - Stop hook 死循环:没检查
stop_hook_active,反复 block 同一条件。Claude Code 在 8 次连续 block 后强制结束 /dev/tty失效:v2.1.139 起 hook 无控制终端。改用terminalSequence- SessionStart hook 拿不到 MCP:SessionStart 在 MCP 连接前触发。需要 MCP 的逻辑放到 PreToolUse 或更晚
下一章:Skills 与自定义命令