Hooks 是 Claude Code 里一段你自定义的 shell 命令,在会话生命周期的固定时点自动执行:写完文件就格式化、跑完任务就测一轮、需要你确认时弹个通知。它的价值在于确定性,这些动作每次都会发生,不依赖模型自己想起来去做。 2026 年 10 月,我在 Windows 10(Git Bash + PowerShell 混用环境)上给 Claude Code 配了三个钩子:PostToolUse 自动格式化、Stop 收尾跑测试、Notification 桌面提醒。官方文档把事件类型讲得很全,示例却清一色 macOS 加 bash,照抄到 Windows 上三个里坏两个,这篇把踩过的坑按顺序写下来。
上一篇写 VPS 探针选型对比,回到 AI 编程工具这条线。Claude Code 接 MCP 之后能摸到本地环境,但摸完之后总有些收尾动作要人盯着:改完文件忘了格式化、任务说跑完了测试没跑。Hooks 补的就是这一段。

Hooks 到底在哪些时点触发?
结论:常用的是八种事件,按「工具执行前后、会话开始结束、需要输入时」分三组,选错了事件等于白配。
| 事件 | 触发时点 | 典型用法 |
|---|---|---|
| PreToolUse | 工具执行前 | 拦截危险命令(rm -rf、强推) |
| PostToolUse | 工具执行后 | 改完文件自动格式化、lint |
| PermissionRequest | 权限弹窗出现前 | 自动放行固定测试命令 |
| UserPromptSubmit | 你提交提示词时 | 注入项目上下文 |
| Notification | Claude 需要你确认时 | 桌面通知 |
| Stop | Claude 结束响应时 | 自动跑测试、生成摘要 |
| SessionStart | 会话开始/恢复 | 加载环境变量、TODO 列表 |
| PreCompact | 上下文压缩前 | 备份转录记录 |
配置写在 ~/.claude/settings.json(项目级用项目根的 .claude/settings.json),结构是三层:事件名 → matcher(正则,过滤工具)→ 命令列表。配完在 Claude Code 里敲 /hooks 能看到是否加载成功,这一步别省,JSON 写错了它是静默失效的,界面上一丝异常都没有。

第一步:PostToolUse 自动格式化,Windows 上第一个坑就在这
结论:官方示例的 jq 管道在 Windows 上跑不通,用 npx 调 prettier 是最稳的写法。
官方文档给的后处理示例长这样:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }
]
}
]
}
}
我的环境 jq 没装,这条命令第一次触发就报 jq: command not found。两个选择:装 jq,或者绕开管道。我选了后者,让 Node 自己解析 stdin:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{const p=JSON.parse(d).tool_input.file_path;if(/\\.(js|ts|json|css)$/.test(p))require('child_process').execSync('npx prettier --write \"'+p+'\"',{stdio:'inherit'})}catch(e){}})\""
}
]
}
]
}
}
难看,但跨平台。matcher 用 Edit|Write 表示写和改都触发。hook 收到的输入是 JSON,从 stdin 进来,里面带工具名和参数,tool_input.file_path 就是刚被改动的文件。

第二步:Stop 钩子跑测试,小心死循环
结论:Stop 事件在 Claude 结束响应时触发,钩子里如果返回非零退出码,Claude 会继续干活,然后再次触发 Stop,配不好就是个死循环。
我的用法是 Stop 时自动跑一遍单元测试,失败就让 Claude 看到:
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "npm test --silent" }
]
}
]
}
}
第一版跑出了循环:测试失败返回非零,Claude 收到错误开始修,修完又触发 Stop,测试又挂在另一个用例上,来回跑了四轮才停。退出码语义在这里是硬约束:PreToolUse 里 exit 2 直接拦截工具调用,Stop 里非零意味着「任务没完成,继续」。想要「跑完通知我但别让它接着修」,命令末尾补一个 ; exit 0 就够了,测试结果会进上下文,但不驱动下一轮。
判断要不要拦,看场景:写库阶段的 Stop 挂测试确实该让它继续修;但如果你的项目测试要联网、要真库,这种钩子就是灾难,先想清楚测试套件能不能无人值守跑。

第三步:Notification 桌面通知,Windows 的命令不一样
结论:macOS 用 osascript,Windows 用 PowerShell 的 BurntToast 或原生 msg,最轻的做法是 PowerShell 的 [System.Media.SystemSounds]。
官方示例是 macOS 的 osascript,Windows 等价写法我实测了两条路。轻量版,只有提示音:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "powershell -NoProfile -Command \"[System.Media.SystemSounds]::Exclamation.Play()\"" }
]
}
]
}
}
带气泡通知的版本,依赖 Windows 10 自带的 Windows.UI.Notifications,写起来长,我最后用的是开源的 BurntToast 模块(Install-Module BurntToast 装一次):
powershell -NoProfile -Command "New-BurntToastNotification -Text 'Claude Code', '等你确认'"
配完一定要真触发一次验证。我第一次配完等了半小时没动静,排查后发现是 settings.json 里少了个逗号,整个 hooks 块没加载。/hooks 界面会显示每个事件的已注册钩子,配完看一眼再干活。

第四步:PermissionRequest 自动放行重复确认
结论:对每天确认几十次的固定命令(npm test、pytest),用 JSON 输出自动放行,matcher 必须写窄。
这个事件的特殊之处是要往 stdout 写 JSON 决策,而不是靠退出码:
{
"hooks": {
"PermissionRequest": [
{
"matcher": "Bash(npm test*)",
"hooks": [
{
"type": "command",
"command": "node -e \"console.log(JSON.stringify({hookSpecificOutput:{hookEventName:'PermissionRequest',decision:{behavior:'allow'}}}))\""
}
]
}
]
}
}
matcher 留空等于放行一切,包括删文件和写配置,这条红线不能碰。文档里专门警告过 .* 这种宽匹配,我的原则是只放行以固定前缀开头的测试命令,其他照常弹窗。

第五步:调试钩子的三个手段
结论:/hooks 看加载、手动喂 stdin 看输出、claude --debug 看触发日志,三层足够定位绝大多数问题。
/hooks:确认钩子真的被加载了,JSON 语法错误在这里现形。- 手动模拟触发:把钩子命令单独拎出来,echo 一段事件 JSON 从 stdin 喂进去,看它自己跑成什么样:
$ echo '{"tool_input":{"file_path":"D:/proj/index.ts"}}' | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(JSON.parse(d).tool_input.file_path))"
D:/proj/index.ts
- 带调试启动:
claude --debug会打印每次 hook 触发和退出码,死循环、静默失败这类问题都在这层露馅。
官方 Hooks 指南在 code.claude.com/docs/en/hooks-guide,事件完整清单和 JSON 输入输出格式在 Hooks reference 页。

踩过的三个坑,按疼的程度排序
结论:jq 缺失只是第一步,Stop 死循环和静默失效才是真功夫,两个都能让你在不知情的情况下白等半小时。
坑一:Stop 钩子死循环。 前文写过,非零退出码驱动 Claude 继续,测试不稳定的项目(flaky test)会转起来停不下来。解法是钩子里只关心「能不能跑」,npm test --silent; exit 0,把修不修的决定权留给模型。
坑二:settings.json 静默失效。 少个逗号、多个注释(settings.json 不支持 JSONC 注释,wrangler 那种 .jsonc 写法搬过来会坏),hooks 整个块不加载,Claude Code 不报错。现在我的习惯是每次改完配置先敲 /hooks 确认。
坑三:Windows 命令的引号转义。 JSON 里的命令字符串再套一层 shell 引号,PowerShell 的单引号双引号规则和 bash 不同,嵌套三层之后人脑编译不动了。我的做法是把复杂逻辑挪到 node 脚本文件里,settings.json 里只写 node D:/scripts/hooks/posttooluse.js 这一行,转义问题一次解决。
常见问题
Q:Hooks 和 MCP 有什么区别,该用哪个?
A:MCP 给模型加能力(能查数据库、能调 API),Hooks 给流程加确定性(每写必格式化、每停必测试)。两者不冲突,我 MCP 接了本地文件系统,Hooks 管收尾纪律。
Q:钩子命令在哪执行,能用 bash 吗?
A:Claude Code 在 Windows 上有自己的 shell 执行环境,跨平台最稳的是 node/python 这类解释器命令,bash 专属语法(管道接 jq、direnv)在 Windows 上别指望,Git Bash 装了也不代表钩子运行时用的是它。
Q:钩子会不会拖慢响应?
A:同步钩子会。PostToolUse 每次写文件都跑 prettier,大项目冷启动要两三秒。我的做法是 prettier 只对改动文件跑(--write 单文件),比全仓格式化快一个量级。官方也支持异步钩子,通知类的建议都走异步。
Q:项目级和用户级配置会冲突吗?
A:不会覆盖,是叠加执行。同一个事件下,用户级和项目级的钩子都会跑,顺序是先用户后项目。团队统一规范放项目级 .claude/settings.json 进版本库,个人偏好放用户级。