Hooks 钩子系统让规则自动执行
Day 07 你认识了 Hooks——今天把它推到生产级。
9 种钩子事件覆盖整个会话生命周期;Shell 与 HTTP 两类处理器各管一摊;
自动格式化、自动测试、阻断危险命令、记录审计日志,全部交给 hook。
Claude 不再依赖你"记得"做这些——规则一旦写进 settings.json,就再也无法被绕过。
思维导图
FIG M — DAY 10 KNOWLEDGE MAP · COMPLETE HOOK SYSTEM
9 种钩子事件
Day 07 只介绍了 PreToolUse 和 PostToolUse——
它们其实只是钩子家族的两位成员。整套系统覆盖了从会话启动到上下文压缩的完整生命周期,
每一个事件都是一个可以注入自动化的接口点。
完整事件列表
| 事件 | 触发时机 | 典型用途 |
|---|---|---|
| PreToolUse | 工具调用之前——可以审查参数、决定放行或阻断 | 阻止危险命令、二次确认、参数注入 |
| PostToolUse | 工具执行之后——结果已写回,可触发副作用 | 自动格式化、运行测试、记录审计 |
| UserPromptSubmit | 用户按下回车提交 prompt 之后、Claude 收到之前 | 注入项目上下文、PII 脱敏、敏感词拦截 |
| SessionStart | 每个 Claude Code 会话刚开始时 | 读取项目元数据、激活上下文、欢迎横幅 |
| SessionEnd | 会话结束/退出时 | 导出对话日志、统计 token 用量 |
| Stop | Claude 主动结束本轮回答时 | 推送桌面通知、发送 Slack 完成提醒 |
| SubagentStop | 子代理(subagent)任务完成时 | 聚合多个 agent 结果、并行任务汇总 |
| PreCompact | 上下文即将被自动压缩之前 | 持久化关键信息到 memory、保存草稿 |
| Notification | Claude 需要用户注意(如等待权限确认)时 | 系统通知中心 / 微信机器人提醒 |
事件在会话中的时间线
FIG 01 · CONVERSATION TIMELINE — HOOK FIRING ORDER
配置与匹配语法
所有 hook 都在 settings.json 中声明。
结构是固定的三层嵌套——事件 → matcher → 处理器——
把这个心智模型刻进脑子,剩下的都是细节。
骨架结构
// .claude/settings.json { "hooks": { // 第 1 层:事件名 "PostToolUse": [ { // 第 2 层:matcher(哪些工具会触发) "matcher": "Edit|Write|MultiEdit", // 第 3 层:处理器列表 "hooks": [ { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format.sh", "timeout": 30 } ] } ] } }
Matcher 写法
| 写法 | 含义 | 示例 |
|---|---|---|
| "Bash" | 精确匹配单个工具名 | 只在 Bash 工具调用时触发 |
| "Edit|Write" | 正则 OR——匹配多个工具 | 编辑或写入文件时都触发 |
| "mcp__.*" | 正则模式——匹配所有 MCP 工具 | 对所有外部 MCP 调用进行审计 |
| "" 或 "*" | 留空 / 通配——匹配所有工具 | 全局日志记录 |
| 注意 | matcher 只在 PreToolUse 与 PostToolUse 上生效——其他事件不需要 matcher | — |
stdin/stdout 协议
Hook 脚本通过 stdin 接收一个 JSON 对象(包含工具名、参数、上下文),通过 stdout 返回处理结果,通过 退出码 表达通过/拒绝。
FIG 02 · HOOK IO PROTOCOL — STDIN / STDOUT / EXIT
退出码语义
静默通过
一切正常,Claude 看不到 hook 跑过的任何痕迹。stdout 内容会被丢弃。
阻断 + 反馈
阻止工具继续执行;stderr 的内容会作为反馈传给 Claude,让它知道为什么被阻止、可以怎么改。
非阻断警告
不阻断,但 stderr 会被 Claude 看到——适合"提醒但不强制"的场景。
自动格式化 & 自动测试
所有 hook 用法中,自动格式化 与 自动测试 是收益最高的两类。 每次 Claude 修改完文件,立刻自动 prettier / black / pytest, 让 Claude 自己看到结果——这是把 LLM 编程从"凑合能跑"提升到"工业级可信"的关键习惯。
Pattern A · 编辑后自动格式化
// .claude/settings.json { "hooks": { "PostToolUse": [{ "matcher": "Edit|Write|MultiEdit", "hooks": [{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format.sh" }] }] } }
#!/bin/bash — .claude/hooks/format.sh # 读取 stdin 拿到改动文件路径 file=$(jq -r '.tool_input.file_path // empty') [ -z "$file" ] && exit 0 case "$file" in *.ts|*.tsx|*.js|*.jsx) npx prettier --write "$file" ;; *.py) black --quiet "$file" ;; *.go) gofmt -w "$file" ;; *.rs) rustfmt "$file" ;; *.json|*.yml|*.yaml) npx prettier --write "$file" ;; esac # exit 0 —— 静默通过,Claude 看不到,但磁盘上的文件已经被格式化了
Pattern B · 修改源码后自动测试
#!/bin/bash — .claude/hooks/auto-test.sh file=$(jq -r '.tool_input.file_path // empty') [ -z "$file" ] && exit 0 # 只对 src/ 下的代码触发测试,跳过 docs / config [[ ! "$file" =~ ^src/ ]] && exit 0 # 只跑相关测试,不是全量——速度优先 test_file="${file//src/test}" test_file="${test_file%.ts}.test.ts" if [ -f "$test_file" ]; then if ! npx vitest run "$test_file" 2>&1 | tail -20; then # 测试失败 → 把失败信息写到 stderr + exit 2 阻断 echo "⚠ 测试失败:$test_file,请修复后再继续" >&2 exit 2 fi fi exit 0
何时阻断、何时静默
格式化、排版、import 排序
幂等操作——无论运行多少次结果一样,没有副作用。让 Claude 不知道有 hook 在跑,体验最丝滑。
测试失败、Lint 报错、类型错误
必须修复才能继续的"硬错误"。stderr 写清楚错误位置和原因,Claude 下一步会自动尝试修复。
性能下降、覆盖率掉、deprecation
不阻断流程但要让 Claude 知道——它可以选择立刻处理或忽略到最后。
危险命令、生产数据库、敏感文件
无论如何不能放行。PreToolUse 拦截 + 退出码 2 + 清晰的 stderr 说明为什么。
Pattern C · UserPromptSubmit 注入项目上下文
#!/bin/bash — .claude/hooks/inject-context.sh # 在每次 prompt 提交前自动塞入当前 git 状态 { echo "=== 当前 git 状态 ===" git status --short echo "=== 最近 3 个 commit ===" git log --oneline -3 } | jq -Rs '{ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: . } }'
Shell 钩子 vs HTTP 钩子
Hook 的处理器有两种形态:本地 shell 命令,或调用远端 HTTP webhook。 前者更快、更私密;后者跨机器、跨团队、能集成 Slack/PagerDuty 这类协作工具。
对比一览
| 维度 | Shell 钩子 | HTTP 钩子 |
|---|---|---|
| 延迟 | ~10–500ms(本地) | ~100ms–数秒(依赖网络与对端) |
| 权限 | 继承当前用户、可以读写本地文件 | 无本地权限,只能影响远端服务 |
| 跨机器 | 否——只能在执行 Claude Code 的机器上跑 | 是——可以触发任意能接收 HTTP 的服务 |
| 团队共享 | 每个开发者要在本地有这个脚本 | 统一在服务端配置,所有人共享 |
| 典型场景 | 格式化、测试、lint、阻断危险命令 | Slack 通知、审计日志、CI 触发、合规上报 |
HTTP 钩子配置
// settings.json — HTTP 类型 hook { "hooks": { "PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "webhook", "url": "https://audit.internal.corp/claude-bash", "method": "POST", "headers": { "Authorization": "Bearer ${AUDIT_TOKEN}" }, "timeout": 5 }] }] } }
三种生产模式
Slack 完成通知
用 Stop 事件 + HTTP 钩子调用 Slack incoming webhook——长任务跑完自动 @你。离开终端去吃饭也不会错过结果。
合规审计上报
用 PreToolUse(Bash) + HTTP 钩子把每次 shell 命令推送到中央审计服务。SOC 2 / ISO 27001 必备。
桌面通知中心
用 Notification 事件 + Shell 钩子调 terminal-notifier / notify-send——Claude 需要确认时自动弹通知。
Shell + HTTP 组合示例
#!/bin/bash — .claude/hooks/audit-with-slack.sh # 既写本地 audit log,也推 Slack——Shell 钩子里调 HTTP payload=$(cat) # 从 stdin 读 JSON tool=$(echo "$payload" | jq -r '.tool_name') cmd=$(echo "$payload" | jq -r '.tool_input.command // ""') # 本地审计 log echo "[$(date -Iseconds)] $tool: $cmd" >> ~/.claude/audit.log # 高危命令推 Slack if [[ "$cmd" =~ (rm -rf|drop table|DELETE FROM) ]]; then curl -s -X POST "$SLACK_WEBHOOK" \ -H "Content-Type: application/json" \ -d "{\"text\":\"⚠ Claude attempted: $cmd\"}" fi exit 0
调试与最佳实践
Hook 不是"写完就忘"——它会在生产环境跑成百上千次。 下面是把 hook 调到稳定可信的几个关键习惯。
调试命令
/hooks
在 Claude Code 里直接打——列出当前会话所有激活的 hook、它们的 matcher 与处理器路径。
claude --debug
启动 Claude 时加这个标志,每次 hook 触发都会在 stderr 打印 stdin / stdout / 退出码——调试金标准。
本地手动跑
把样例 JSON echo '...' | ./hooks/format.sh 喂给脚本,直接看输出——比在 Claude 里调试快 10 倍。
tail -f audit.log
所有 hook 都写本地日志,开第二个终端 tail 它——任何不对劲都能立刻看见。
8 条铁律
- 始终给 hook 加 timeout——默认 60s,但格式化只要 5s 就够,超时 = 用户卡死
- 用
${CLAUDE_PROJECT_DIR}而不是相对路径,避免 cwd 漂移 - 幂等:同样的输入跑 100 次结果必须一致——hook 不该有副作用顺序依赖
- 失败要"响":stderr 写清楚哪个文件、哪一行、为什么,让 Claude 能自动修
- 分离环境:开发环境的 audit 别送到生产审计端点
- 团队 hook 进 git(
.claude/settings.json),个人偏好放settings.local.json - HTTP hook 必须配 timeout 和重试上限——远端挂了别拖累本地
- 用
jq而不是 grep——hook 的输入是 JSON,正则会咬人
常见反模式
PostToolUse 跑全量测试
每次 Edit 都跑 npm test——单次几分钟,会让 Claude 的每一步都变成等待。改成只跑相关测试或挪到 Stop 事件。
PreToolUse 调用外部 API
每次工具调用前都打 HTTP 校验——网络一抖就阻塞所有操作。改成异步或挪到 PostToolUse。
失败不打日志
hook 静默退出非 0——Claude 收不到反馈、你看不到为什么。永远写 stderr。
hook 里改 Claude 改过的文件再 exit 0
Claude 不知道文件被改过、后续可能写脏。要么 exit 2 强制让 Claude 重新读,要么明确告诉它"文件已被 hook 重写"。
Labs
四个递进的动手实验——从最简单的格式化 hook 到完整审计流。
预计 45 分钟,建议在一个真实项目里跑通,每个 lab 结束后用 /hooks 命令检查。
自动格式化 hook
在 .claude/settings.json 写一个 PostToolUse matcher "Edit|Write",hook 调用 prettier --write。让 Claude 改一个 .ts 文件,确认磁盘上的格式被自动统一。
失败阻断的测试 hook
在 Lab 01 基础上加 PostToolUse 跑相关测试。故意让 Claude 改一个会让测试挂的地方,确认它通过 stderr 收到失败反馈并主动尝试修复。
UserPromptSubmit 注入上下文
写一个 UserPromptSubmit hook,在每次 prompt 提交时自动把 git status、当前分支、最近 3 个 commit 注入上下文。开两个会话对比有 / 没有这个 hook 时 Claude 回答的差别。
Slack 通知 hook(HTTP)
在 Slack 创建一个 incoming webhook,配一个 Stop 事件的 HTTP 钩子——Claude 每完成一轮回答都自动发一条带摘要的消息到指定频道。
常见问题
Q · 01 Hook 和 Claude 的指令哪个优先级高?Claude 能绕过 hook 吗? +
Hook 绝对优先,Claude 完全无法绕过。Hook 是由 Claude Code 进程本身执行的——它在工具调用流水线的硬编码节点上触发,不是"礼貌请求"。哪怕你在 prompt 里说"请跳过格式化 hook",Claude 也做不到。
这就是为什么 hook 是把安全策略落地的最佳载体:把"不允许 rm -rf /"编码成 hook,比写在 CLAUDE.md 里告诉 Claude "请不要" 可靠一万倍。
Q · 02 Hook 和 Day 07 的 Skill 是什么关系? +
互补——一个是规则,一个是能力。Hook 是"每次 X 发生时自动做 Y"——它在事件触发时被动跑;Skill 是"用户说 /foo 时执行预定义工作流"——它需要被显式调用。
典型配合:写一个 /deploy Skill 把部署流程封装好,再写一个 PreToolUse Hook 检查 Skill 里的 git push 是不是推到了正确分支——Skill 提供便捷,Hook 守住安全底线。
Q · 03 为什么我的 hook 没触发?怎么排查? +
按这个顺序排:(1) /hooks 命令看 Claude Code 是否识别到了你的配置——如果列表里没有,说明 settings.json 没加载(可能是路径错误或 JSON 语法错);(2) claude --debug 看 hook 是否被尝试触发但失败;(3) 手动 echo '{}' | ./hooks/your.sh 跑一遍——脚本本身有没有执行权限?shebang 对不对?
最常见的 3 个坑:忘了 chmod +x、matcher 写成 "Bash*"(不支持 glob,只支持正则——应该是 "Bash.*")、相对路径在 hook 运行时 cwd 不是你以为的位置(永远用 ${CLAUDE_PROJECT_DIR})。
Q · 04 PostToolUse 修改了文件,Claude 怎么知道? +
Claude Code 在 PostToolUse 之后会重新读取文件 metadata——但它的内存缓存可能仍是 hook 改之前的内容。这就是 Anti-Pattern 04 提到的坑。
两个解决方案:(1) 让 hook 用 stdout 返回一个特殊 JSON,明确告知 Claude "文件已被修改、需重读";(2) 格式化这类"小改动"通常 Claude 下次 Read 时会发现差异并自动适应,对体验影响不大。如果是"重写整个文件"这种大改,强烈建议用方案 1。
Q · 05 CI 环境中 hook 还会运行吗? +
会——只要你在 CI 里用的是同一个项目的 .claude/settings.json。这通常是好事(CI 里也跑格式化和测试),但要注意两点:
(1) CI 环境可能没有本地脚本依赖(prettier、black 等)——确保 CI 工作流的 npm ci/pip install 装齐了;(2) 把会发通知的 hook(Slack 提醒、桌面通知)放进 settings.local.json 或用环境变量门控——不然每次 CI 跑都发 Slack 消息,所有人会想暴打你。
Q · 06 Hook 出错了 Claude 会崩溃吗? +
不会崩,但行为分两种情况:(1) PreToolUse hook 出错且 exit 非 0——Claude 会按"被阻断"处理,工具不会执行,stderr 内容传给 Claude 看;(2) PostToolUse hook 出错——工具已经执行完了,hook 错误不会回滚结果,只会被记录到 debug 日志。
所以最危险的不是 hook 崩,是 hook 静默失败——比如格式化脚本写错了路径,每次都 exit 0 但实际什么都没做。这就是为什么要有 audit 日志 + tail -f 监控习惯。
复习题
- 列出至少 6 种 Hook 事件名,说明它们各自的触发时机。
PostToolUse的 hook 如何把"测试失败"反馈给 Claude?退出码与 stderr 各扮演什么角色?- Matcher 字段的
"Edit|Write"与"Edit"有什么区别?为什么用正则不用 glob? - Shell 钩子 与 HTTP 钩子的核心差异是什么?各举一个最合适的场景。
- 请写出三条"hook 反模式"——并说明它们为什么会让用户体验变糟。
自检清单
- 能说出至少 6 种钩子事件及它们的触发时机
- 理解 settings.json 中 hook 的三层嵌套结构(事件→matcher→处理器)
- 能用
"Edit|Write|MultiEdit"这类正则 matcher 精确选定触发工具 - 掌握 exit 0 / exit 2 / 其他退出码的语义差异
- 已为项目写好
PostToolUse自动格式化 hook - 能用
/hooks与claude --debug调试 hook 行为 - 知道 Shell 与 HTTP 两种处理器各自的适用场景
- 了解 4 条常见反模式,并能在自己的 hook 中规避
延伸阅读
Claude Code — Hooks Reference
官方 hooks 完整文档:所有事件名、JSON schema、退出码协议、最新可用的 hook 类型。
Claude Code — Settings 配置层级
全局 / 项目 / 本地三层 settings.json 优先级与合并规则——决定 hook 在哪里生效。
awesome-claude-hooks
社区维护的 hook 模板集合:自动格式化、安全审计、Slack 集成、git 工作流自动化等。
Day 11 预告
自定义 Skills — 把重复工作封装成一句命令
今天你学会让规则自动执行——明天你将教 Claude 学一项你的"祖传手艺"。在 .claude/skills/ 写一个 markdown 文件就能创建斜杠命令,把你常用的多步工作流(PR 评审、bug 复现、deploy 流程)封装成 /review、/repro、/deploy,团队成员一键复用。Hook 守住底线,Skill 提供便捷——双剑合璧。