让 Claude Code 自己收尾:Hooks 在 Windows 上的配置实测(2026)

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 补的就是这一段。

Claude Code 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 就是刚被改动的文件。

步骤 1:PostToolUse 格式化钩子

第二步: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 挂测试确实该让它继续修;但如果你的项目测试要联网、要真库,这种钩子就是灾难,先想清楚测试套件能不能无人值守跑。

步骤 2: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 界面会显示每个事件的已注册钩子,配完看一眼再干活。

步骤 3:Windows 通知钩子

第四步: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 留空等于放行一切,包括删文件和写配置,这条红线不能碰。文档里专门警告过 .* 这种宽匹配,我的原则是只放行以固定前缀开头的测试命令,其他照常弹窗。

步骤 4:权限自动放行

第五步:调试钩子的三个手段

结论:/hooks 看加载、手动喂 stdin 看输出、claude --debug 看触发日志,三层足够定位绝大多数问题。

  1. /hooks:确认钩子真的被加载了,JSON 语法错误在这里现形。
  2. 手动模拟触发:把钩子命令单独拎出来,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
  1. 带调试启动:claude --debug 会打印每次 hook 触发和退出码,死循环、静默失败这类问题都在这层露馅。

官方 Hooks 指南在 code.claude.com/docs/en/hooks-guide,事件完整清单和 JSON 输入输出格式在 Hooks reference 页。

步骤 5:调试手段

踩过的三个坑,按疼的程度排序

结论: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 进版本库,个人偏好放用户级。

相关阅读

上一篇 Ansible 批量初始化 VPS:一条命令完成用户、密钥、防火墙基线(2026)