Claude Code 实战第二课:Hooks 钩子自动化,把「人盯」变成「自动把关」
生命不息,折腾不止。上一课给 Claude Code 装好了「手脚」和「习惯」,这一课再给它立「规矩」——用 Hooks 在它每次动手前后自动挂上检查,改代码先跑测试、提交前先过 lint,把「人盯着才放心」变成「代码自己把关」。
上篇结尾我把话说在前头了:第一课聊 MCP 和自定义命令,第二课聊 Hooks。今天就来兑现。这玩意儿是 Claude Code 里最容易被忽略、又最能让它「上生产」的能力——不靠模型记性,靠写死的代码,每次触发、无一例外。
一、为啥要 Hooks:那 5% 的翻车,靠「记得提醒」防不住
Claude Code 平时是真能干,问题是它偶尔会自信地犯错:把改动推到 main、跳过格式化、提交没过 lint 的代码、顺手 rm -rf 掉不该删的目录。这类事故概率不高,可一旦发生就伤筋动骨。
有人觉得「我在 system prompt 里写一句『提交前先跑测试』不就完了吗」。现实是这一句不靠谱:
- 模型会忘,prompt 越写越长,它抓不住重点;
- 模型会糊弄,觉得「这次改动很小不用测」;
- 换个人、换个模型,这句提示词就跟着换,约束力全看运气。
Hooks 解决的就是这个「运气象」。 它是确定性门控:把规则写成 shell 脚本,挂在 Claude Code 的生命周期节点上,每次到点必触发,跟模型当时怎么想一毛钱关系都没有。一句话——你现在手动盯着它干的那些「检查」,全可以让脚本替你干。
二、Hooks 是什么:挂在生命周期节点上的「自动开关」
Hooks 说白了就是一段 shell 命令,Claude Code 在特定节点会自动执行它。比如「工具调用之前」「文件被编辑之后」「这一轮回答结束」。你写的脚本从 stdin 读一段 JSON(Claude 把上下文塞在里面),干完活之后用退出码说话。
最关键的两个事件先记住:
PreToolUse:某个工具执行前触发,能拦住它。这是安全门禁的核心。PostToolUse:某个工具执行成功后触发,做「事后检查」,比如格式化、跑测试。
此外官方还有一长串事件,常用的有这么几个:
| 事件 | 什么时候触发 | 能干吗 |
|---|---|---|
SessionStart / SessionEnd |
会话开始/结束 | 初始化、清理、压缩后重注入上下文 |
UserPromptSubmit |
你提交提示词的瞬间 | 校验或给提示词补上下文 |
PostToolUse |
工具执行成功后 | 格式化、跑测试、记日志 |
Stop |
Claude 这一轮回答结束 | 收尾检查、提醒你 review |
Notification |
Claude 发通知时 | 转发到 Slack、桌面弹窗 |
SubagentStop |
子代理跑完时 | 监控子代理、抓摘要 |
PreCompact / PostCompact |
上下文压缩前后 | 压缩前存草稿、压缩后补关键记忆 |
配置就一个文件:.claude/settings.json(项目级,放仓库根目录,可 git 提交给全团队共用);个人全局的放 ~/.claude/settings.json。结构是三层嵌套:事件 → 匹配器(matcher)→ 具体 hook。骨架长这样:
1 | { |
几点说清楚:
matcher是个正则,匹配的是工具名(tool_name)。Bash匹配所有命令行、Edit|Write匹配改文件、Read/Glob/Grep匹配读和搜、mcp__github__.*匹配某个 MCP 工具。不写 matcher(或写成空)就每次都触发。- 数据怎么拿:脚本从 stdin 读 JSON,里面有
tool_name和tool_input。Bash的命令在.tool_input.command,文件类工具的路径在.tool_input.file_path。 - 结果怎么回:退出码说话——
0放行、2拦截(stderr 会回传给 Claude 当理由)、1只记条警告不拦。 - 配置里可以用
${CLAUDE_PROJECT_DIR}这个环境变量指到项目根目录,团队共享时别写死本机路径。
三、动手写第一个 Hook:危险命令「一票否决」
第一个 hook 做最有价值的事——拦住 rm -rf、git push -f、git reset --hard 这类危险命令。在项目里建个脚本 .claude/hooks/block-rm.sh:
1 |
|
这一步很关键:脚本必须给执行权限,否则 Claude Code 不会跑它:
1 | chmod +x .claude/hooks/block-rm.sh |
再在 .claude/settings.json 里挂上 PreToolUse + Bash 匹配器(就是上面那个骨架)。现在 Claude 每次想跑 bash 命令,这个脚本都会先过一遍。
这里有个新手最容易踩的坑:退出码。exit 2 才是「拦截」,exit 1 只算「非阻塞错误」——命令照样会执行,你的安全门禁形同虚设。所以安全类 hook 必须 exit 2,别图省事写 1。手动测一下你就明白了:
1 | echo '{"tool_input":{"command":"rm -rf /opt/old"}}' | bash .claude/hooks/block-rm.sh |
四、进阶:提交前自动跑 lint,过不了就别想 commit
光拦危险命令还不够,更常见的翻车是「提交了没过 lint 的代码」。用同样思路,在 git commit 之前插一道 PreToolUse 门禁:
1 |
|
chmod +x 后,在 settings.json 的 PreToolUse 里再加一条指向它。以后 Claude 想 git commit,会先跑一遍 npm run lint;不过关就 exit 2,stderr 里的报错会直接甩回给 Claude,逼它先修再提。
PreToolUse 管「动手前」,它兄弟 PostToolUse 管「动手后」。比如每次 Claude 改完文件,自动给你格式化回来,用的是 Edit|Write 匹配器 + jq 提取文件路径:
1 | { |
跑测试也一个道理:改到 Python 文件就自动 pytest 对应测试,改到 JS 就自动跑对应的单测。把这些组合起来,你就有了一个「改完自动格式化、提交自动过 lint、危险命令自动拦」的 Claude Code。
五、装完怎么验,以及四条保命提醒
配好之后先验证,别急着上生产:
- 在 Claude Code 会话里敲
/hooks,会打开一个 hooks 浏览器,能看到每个事件挂了几个 hook、状态对不对。 - 独立测脚本:手动把示例 JSON 喂进去(就是第三小节那种
echo ... | bash xxx.sh)。脚本在外面就报错,进去必然也报错。 - 调试信息写 stderr:hook 写进 stderr 的内容会出现在 Claude 的上下文里,开发阶段
echo "DEBUG: matched $cmd" >&2能帮你定位,稳定了再删。
最后四条提醒,都是血泪教训:
- hook 要快。它是同步执行的,一个 5 秒的 hook 就会让每次匹配的工具调用都多等 5 秒。尽量控制在 2 秒内,理想是半秒内搞定;重的活(全量测试)留给 CI。
jq路径写错会静默返回null,条件就永远不匹配,hook 看似在跑其实没生效。所以先手动喂真实 JSON 测jq表达式。- 别把密钥写进可共享的
.claude/settings.json。团队共享时用绝对路径 + 环境变量,token 走${VAR:-默认值}占位,别把真实 key 提交进仓库。 - Hooks 不是 CI、也不是 code review 的替代品。它是本地第一道防线,PR 上的完整流水线该跑还得跑。
到这,Claude Code 第二课就完了。回头看,你的 Claude Code 已经有了「手脚」(MCP)、「习惯」(自定义命令)、「规矩」(Hooks)三层,从「会聊天的助手」正式进化成了「能自动把关的工程师」。
生命不息,折腾不止。下一篇《Claude Code 实战第三课:CLAUDE.md + 权限模式,把项目约定和安全边界沉淀成文件》——Hooks 管的是「动作」,第三课管的是「记性和底线」:怎么写 CLAUDE.md 让 Claude 读项目、守住约定,怎么用 permission 规则把「哪些操作自动放行、哪些必须问你」写进 settings.json,让 Claude Code 既有脑子又懂分寸。下篇见。