本教程聚焦 Claude Code 的 Hooks(钩子) 系统——一组在 Agent 生命周期固定节点上"确定性"执行的脚本。与"在 CLAUDE.md 里写一句『记得跑测试』然后祈祷模型照做"不同,Hooks 保证某些动作一定会发生。我们将沿着会话从启动到结束的完整事件链,用 SessionStart、UserPromptSubmit、PreToolUse、PostToolUse、Notification、Stop、PreCompact、SessionEnd 等九个事件,亲手织出一张"自格式化 + 防破坏 + 质量门禁 + 全程审计 + 上下文抢救"的自动化闭环。适用人群:已能独立使用 Claude Code 完成 CRUD、希望把团队规范"焊死"进工具链的中高级开发者与 DevOps/平台工程师。
claude --version 可输出版本)jq(jq --version)ruff/pytest)理解一句话:Hooks 把"希望发生的事"变成"必然发生的事"。 模型可能忘记、可能偷懒,但一个挂在 PreToolUse 上、exit 2 的 shell 脚本,无论模型多想跑 rm -rf /,都会被硬性拦下。
Claude Code 在 2026 年的 Hooks 系统提供 五类处理器(handler type),都挂在 settings.json 的 hooks 键下:
| type | 运行方式 | 典型场景 |
|---|---|---|
command |
执行 shell 命令,读 stdin、写 stdout/stderr | 99% 的确定性规则 |
http |
POST 事件 JSON 到一个 URL | 团队共享审计服务 |
mcp_tool |
调用已连接 MCP 服务器的工具 | 复用既有 MCP 能力 |
prompt |
单轮 LLM 判定(默认 Haiku,30s 超时) | 需要"判断"而非死规则 |
agent |
多轮子代理验证(可读文件、跑命令,60s/最多50轮) | 要核对代码库真实状态 |
退出码三态语义(所有事件通用):
exit 0:无异议,动作照常进行。对 UserPromptSubmit/SessionStart,stdout 会被注入到模型上下文。exit 2:阻断。stderr 作为反馈喂回模型。部分事件(SessionStart、Notification 等)不可阻断,exit 2 只是把 stderr 展示给人。<hook name> hook error 提示。预期结果:理解到——确定性靠 command,模糊判断靠 prompt,深度核对靠 agent。本教程主走 command,在第 7 步引入 agent 做质量门禁。
Hooks 可挂在五个位置,作用域由小到大:
| 位置 | 作用域 | 可共享 |
|---|---|---|
~/.claude/settings.json |
你所有项目 | 否(本机) |
.claude/settings.json |
单个项目 | 是(可提交进仓库) |
.claude/settings.local.json |
单个项目 | 否(自动 gitignore) |
| 企业托管策略 | 全组织 | 是(管理员控制) |
插件 hooks/hooks.json |
插件启用时 | 是(随插件分发) |
先建项目级骨架。在仓库根创建 .claude/settings.json:
{
"hooks": {
"SessionStart": [],
"UserPromptSubmit": [],
"PreToolUse": [],
"PostToolUse": [],
"Notification": [],
"Stop": [],
"PreCompact": [],
"SessionEnd": []
}
}
操作说明:保存后,在 Claude Code 内输入 /hooks,应能看到按事件分组的空列表。
常见问题:/hooks 显示为空——多半是 JSON 非法(不允许尾逗号、不允许注释)。用 jq . .claude/settings.json 校验。另注意:个别版本(如 v2.1.120 在 VSCode 原生扩展下)会静默丢弃 ~/.claude/settings.json 里的用户级 Hooks,遇到就改挂项目级。
SessionStart 在会话开始、/clear、resume、compact 时触发,matcher 取 startup|resume|clear|compact。它不可阻断,但 stdout 会被注入上下文。
两个高频用法:自举环境与压缩后续命。先做自举——把 direnv 注入到 Bash 工具的执行前导(CLAUDE_ENV_FILE):
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "direnv export bash > \"$CLAUDE_ENV_FILE\"" }
]
}
]
CLAUDE_ENV_FILE 的内容会被 Claude Code 作为脚本前导拼到每条 Bash 命令前,于是 Bash 工具也能"继承"目录级环境变量,省去反复 export。
预期结果:进入项目后第一条 Bash 命令即可用 .envrc 里的变量。
再做"压缩续命"——上下文压缩会丢细节,用 compact matcher 把关键约定重新塞回去:
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo '提醒:用 ruff format 而非 black;提交前必须 pytest 全绿;当前迭代=支付重构。'"
}
]
}
常见问题:每次 startup 都注入大段文本会浪费 token——静态长文优先写进 CLAUDE.md,Hook 只放"动态、易变"的少量上下文(如 git log --oneline -5)。
在你提交一条 prompt、模型处理之前触发,无 matcher,默认超时仅 30s。exit 2 可阻断处理;更优雅的是返回 additionalContext 注入上下文:
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "git log --oneline -3 | sed 's/^/最近提交: /'"
}
]
}
]
预期结果:每次提问时,模型都能"看到"最近三条提交,减少让它翻历史的需求。
常见问题:脚本卡住会拖垮体验(30s 超时)。任何可能阻塞的命令(如联网请求)都别放在这里。
在工具执行之前触发,可阻断。matcher 匹配工具名(Bash、Edit|Write、或正则 mcp__memory__.*)。这是把安全策略"焊死"的核心。
先拦最危险的 rm -rf。写脚本 .claude/hooks/block-rm-rf.sh:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
if echo "$COMMAND" | grep -Eq 'rm[[:space:]]+-[a-zA-Z]*r'; then
echo "阻断:禁止 rm -rf,请改用逐项删除或 git clean。" >&2
exit 2
fi
exit 0
chmod +x 后注册:
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm-rf.sh" }
]
}
]
进阶——结构化 JSON 输出比 exit 2 更可控,可区分 allow/deny/ask/defer 并附带理由:
#!/bin/bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
if echo "$CMD" | grep -q 'git push'; then
cat <<'EOF'
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"推送需人工二次确认"}}
EOF
exit 0
fi
exit 0
关键铁律:Hook 能"收紧"权限,但不能放宽。即便 Hook 返回 allow,settings 里的 deny 规则依然生效;反过来,deny 的 Hook 即便在 bypassPermissions 或 --dangerously-skip-permissions 模式下也照拦不误。所以 PreToolUse 是"用户无法绕过的最后一道墙"。
常见问题:脚本不触发——八成是 matcher 写错(大小写敏感,必须精确 Bash),或脚本没 chmod +x,或路径用了相对路径(改用 $CLAUDE_PROJECT_DIR 或绝对路径)。
工具成功执行后触发。注意:它无法撤销已发生的动作,只能"善后"或"反馈"。
自动格式化是最经典用法。Edit|Write matcher + 提取 file_path 喂给 ruff:
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs -r ruff format"
}
]
}
]
再叠一条审计:把每条 Bash 命令记进日志(注意多 Hook 时最严格者胜的合并规则):
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -c '{ts: now|todate, cmd: .tool_input.command, cwd: .cwd}' >> \"$CLAUDE_PROJECT_DIR/.claude/audit.log\""
}
]
}
多 Hook 合并语义:同一事件下所有匹配 Hook 并行跑完再合并。PreToolUse 的权限决策按 deny > defer > ask > allow 取最严;additionalContext 则全部保留。所以一条审计 Hook(exit 0)和一条拦截 Hook(exit 2)可共存——审计照写,命令照样被拦。
常见问题:PostToolUse 用 decision: "block" 虽能把 reason 喂回模型,但文件已改完,无法回滚。要"事前阻断"永远用 PreToolUse。
Notification 在 Claude 等待输入或权限确认时触发。挂一个桌面通知,你就能切去干别的:
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "notify-send 'Claude Code' '需要你的输入'"
}
]
}
]
macOS 用 osascript -e 'display notification ...',Windows 用 PowerShell 的 MessageBox。matcher 可精细到 permission_prompt(只权限确认时响)或 idle_prompt(只在任务完成等下一条 prompt 时响)。
常见问题:macOS 上 osascript 走的是"脚本编辑器"App,若它没有通知权限会静默失败。先在终端跑一次 osascript -e 'display notification "test"',再到"系统设置 > 通知 > 脚本编辑器"打开权限。
Stop 在 Claude 结束回应时触发,无 matcher。返回 {"decision":"block","reason":"..."} 或 exit 2 可阻止停止、强制继续。这正是"质量门禁"的落点:不让 Claude 收工,除非测试全绿。
两种实现强度:
弱门禁(command + Stop)——直接跑 pytest,失败就 block:
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "cd \"$CLAUDE_PROJECT_DIR\" && pytest -q >/tmp/stopgate.log 2>&1 || { echo '测试未通过,详见 /tmp/stopgate.log' >&2; exit 2; }"
}
]
}
]
强门禁(agent 型 Hook)——交给一个能读代码、跑命令的子代理去核对:
"Stop": [
{
"hooks": [
{
"type": "agent",
"prompt": "运行 ruff check 与 pytest。若有任何失败,返回 {\"ok\": false, \"reason\": \"具体失败摘要\"};全绿返回 {\"ok\": true}。"
}
]
}
]
agent 型 Hook 返回 ok:false 时,reason 会被当成下一步指令喂回主 Claude,驱动它去修复。
致命陷阱——死循环与上限:Stop 一旦 block,模型继续干,干完又触发 Stop,极易死循环。Claude Code 的保护是:连续 block 超过 8 次且无进展,会强行覆盖 Hook 放行。正确写法是读 stop_hook_active 字段,若已 true 则放行:
#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # 已经在续跑中,别再拦
fi
cd "$CLAUDE_PROJECT_DIR" && pytest -q || { echo '测试未过,请修复后收尾' >&2; exit 2; }
确实需要更多轮,用环境变量 CLAUDE_CODE_STOP_HOOK_BLOCK_CAP 调高上限。
常见问题:Stop Hook 不触发——它只在"模型主动结束回合"时触发,用户打断(Ctrl+C)不触发;API 报错触发的是独立的 StopFailure 事件(其 matcher 可按 rate_limit/overloaded/billing_error 等过滤)。
PreCompact 在压缩之前触发。趁压缩还没把对话摘要化,把"绝不能丢"的关键状态落盘或回灌:
"PreCompact": [
{
"hooks": [
{
"type": "command",
"command": "git diff --stat | head -20 | sed 's/^/未提交改动: /'; echo '当前任务=实现支付回调;下一步=补单测'"
}
]
}
]
PostCompact 则在压缩后触发,二者配合可做"压缩前快照 + 压缩后恢复"。matcher 支持 manual/auto。
SessionEnd 在会话终止时触发,matcher 取 clear|resume|logout|prompt_input_exit|...。典型用途是清理临时文件与归档审计:
"SessionEnd": [
{
"matcher": "clear",
"hooks": [
{ "type": "command", "command": "rm -f /tmp/claude-scratch-*.txt" }
]
}
]
只在你跑 /clear 时清理,正常退出不触发。
调试三板斧:
/hooks:可视化所有已注册 Hook,按事件分组,最先看这个。Ctrl+O 转录视图:每个触发的 Hook 一行摘要——成功静默、阻断显示 stderr、非阻断错误显示首行 stderr。--debug-file:claude --debug-file /tmp/claude.log 启动,另开终端 tail -f,可看完整匹配/退出码/stdout/stderr。手动测脚本(强烈建议提交前都测一遍):
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./.claude/hooks/block-rm-rf.sh
echo $? # 应为 0
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /tmp/x"}}' | ./.claude/hooks/block-rm-rf.sh
echo $? # 应为 2
JSON 校验失败排查:shell-form 命令经 sh -c 跑,若你的 ~/.bashrc 里有无条件 echo,输出会拼到 JSON 前面导致解析失败。把 profile 里的 echo 包一层:
if [[ $- == *i* ]]; then echo "Shell ready"; fi
$- 含 i 表示交互式,Hook 跑在非交互 shell,echo 被跳过。
关停开关:"disableAllHooks": true 一键全关(托管策略里的 Hook 需要在托管层也设才彻底关);权限、hooks、apiKeyHelper 改完无需重启即热加载。
目标:一个支付服务仓库,达到——编辑即格式化、.env/main 分支不可碰、收尾前 pytest+ruff 必绿、每条命令有审计、压缩不丢任务。完整 settings.json:
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{ "type": "command",
"command": "echo '项目=pay-service;规范=ruff format;收尾前 pytest 全绿;当前迭代=回调幂等'" }
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command",
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/guard.sh\"" }
]
},
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command",
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/protect.sh\"" }
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command",
"command": "jq -r '.tool_input.file_path' | xargs -r ruff format" }
]
},
{
"matcher": "Bash",
"hooks": [
{ "type": "command",
"command": "jq -c '{ts: now|todate, cmd: .tool_input.command}' >> \"$CLAUDE_PROJECT_DIR/.claude/audit.log\"" }
]
}
],
"Stop": [
{
"hooks": [
{ "type": "command",
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/quality-gate.sh\"" }
]
}
],
"Notification": [
{
"matcher": "idle_prompt",
"hooks": [
{ "type": "command",
"command": "notify-send 'Claude Code' '任务完成,等你下一步'" }
]
}
]
}
}
guard.sh(拦 rm -rf、拦 git push origin main):
#!/bin/bash
INPUT=$(cat); CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
if echo "$CMD" | grep -Eq 'rm[[:space:]]+-[a-zA-Z]*r'; then
echo "阻断:禁止 rm -rf" >&2; exit 2
fi
if echo "$CMD" | grep -Eq 'git push[[:space:]].*main'; then
echo "阻断:禁止直推 main,请走 PR" >&2; exit 2
fi
exit 0
protect.sh(拦 .env 改写):
#!/bin/bash
INPUT=$(cat); FP=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
case "$FP" in
*.env|*.env.*) echo "阻断:禁止改写 $FP" >&2; exit 2 ;;
esac
exit 0
quality-gate.sh(带 stop_hook_active 守卫的收尾门禁):
#!/bin/bash
INPUT=$(cat)
[ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ] && exit 0
cd "$CLAUDE_PROJECT_DIR" || exit 0
ruff check . >/tmp/qg.log 2>&1 && pytest -q >>/tmp/qg.log 2>&1 || {
echo "质量门禁未过,摘要:" >&2; tail -5 /tmp/qg.log >&2; exit 2
}
exit 0
运行效果:你让 Claude"给 refund.py 加幂等键",它编辑完自动 ruff format;想 git push origin main 被拦;收尾时 pytest/ruff 没过就被打回继续修,过了才放行;全程 Bash 命令沉淀在 audit.log;上下文压缩后任务上下文被重新注入。这就是一张"用户无法绕过、模型无需记忆"的确定性质量网。
flowchart TD
A[SessionStart<br/>自举环境/压缩续命] --> B[UserPromptSubmit<br/>注入最近提交]
B --> C[PreToolUse<br/>护栏: rm-rf/.env/main]
C -->|exit 2 阻断| Z[打回模型重试]
C -->|放行| D[工具执行]
D --> E[PostToolUse<br/>自动格式化+审计]
E --> F{Stop 质量门禁}
F -->|ruff/pytest 未过| G[decision:block<br/>强制继续修复]
G --> C
F -->|全绿放行| H[Notification<br/>桌面提醒]
I[PreCompact<br/>抢救上下文] -.压缩前.-> A
H --> J[SessionEnd<br/>清理/归档审计]
style C fill:#ffe0e0
style F fill:#fff3cd
style Z fill:#ffe0e0
code.claude.com/docs/en/hooks——全部事件的 stdin/stdout JSON schema、异步 Hook、MCP 工具 Hook。PostToolUse 事件 POST 到统一服务,做跨团队工具调用合规留痕。stop_hook_active 守卫的 Stop 门禁、能否用 --debug-file 排出一个"不触发"的 Hook、能否解释"Hook 能收紧不能放宽"的权限铁律。[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
