Hooks:让 Claude Code 自动化的胶水——给 AI 干活的每个环节装上检查点

提示词叮嘱一百遍,不如程序拦截一次。讲清 Hooks 的三层结构、退出码语义,和一套让 AI 干活有声音的实战配置。

Claude Code
返回文章列表

第 1 篇埋过一个钩子:提示词有三个天花板,其中一个是“不能强制”——你在提示词里写“每次改完代码必须跑测试”,AI 心情好就跑,上下文一长就忘。当时说解决它的东西叫 Hooks。这篇就来兑现。

先看一个所有人都遇过的场景。

“说了多少遍,改完要跑 lint”

你受不了代码风格被改乱,在 CLAUDE.md 里写了一条:“修改任何文件后必须运行 lint”。头两轮它乖乖照做。第四次对话,它在修一个复杂 bug,改完五个文件,直接汇报“搞定了”。你问 lint 呢?它道歉,补跑,又发现两处问题。

这不是它懒,是它的注意力被修 bug 占满了。你那条规矩还躺在上下文里,只是排在第几十条,轮不到它做主。

问题不在你写得不够狠——加十个“必须”也没用。问题在于,提示词在结构上就是建议,而有些事需要制度。制度不靠对方自觉,靠的是流程上的一道关卡:东西不过检,就往下走不了。

这道关卡,就是 Hooks。

Hooks 是什么:流水线上的质检员

一句话说清:Hooks 是你插在 Claude Code 工作流程里的检查点,由程序强制执行,不经过 AI 同意。

它和提示词的差别,类比一下就清楚。提示词像贴在墙上的操作规范,员工每次干完活,指望他自己想起来对照检查;Hooks 像流水线上的质检员,产品从他面前过,不过检就按下停止键——员工想绕都绕不开。

还有一个同样重要的差别:质检员不只用眼睛,还能开口。AI 被关卡拦下时,会收到拦截理由,下一轮它就知道该怎么改。这让它不只是“防呆”,还能“教学”。

机制:事件、匹配器、命令

Hooks 的配置在 settings.json 里(就是第 2 篇提过的那个分层配置体系,这里先不展开,第 7 篇细讲),结构是三层:什么事件发生 → 匹配哪些对象 → 执行什么命令。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "/path/to/your-check.sh" }
        ]
      }
    ]
  }
}

这段配置读作:每当 Claude 要调用 Bash 工具(PreToolUse 事件 + matcher: "Bash"),先执行你的 your-check.sh。脚本拿到工具调用的完整信息(从 stdin 进来,JSON 格式),你爱查什么查什么。

事件是 Hooks 的核心概念,值得单独说。它覆盖了 AI 干活的各个环节,我按用途归成四类,挑代表性的认识一下:

  • 工具调用的前后:PreToolUse(调用前,可以拦截)、PostToolUse(成功后)——最常用的一类,自动格式化、危险命令拦截都靠它;
  • 会话生命周期:SessionStart(开会)、Stop(一轮回答结束)、PreCompact(上下文压缩前)——适合做通知和状态记录;
  • 权限与安全:PermissionRequest(要权限了)、PermissionDenied(自动模式拒了个调用);
  • 环境变化:FileChanged(监控的文件变了)、CwdChanged(工作目录切了)——做环境联动用。

数量上有个值得玩味的现象:素材仓库的文档整理时列了 30 个事件,我发稿前去官方文档核对,已经是 33 个——这个清单还在持续变长。所以别背事件表,用到哪个查哪个,以官方文档为准。

退出码是另一个关键机制,它决定检查结果怎么生效:

  • 脚本以 0 退出:放行,一切照旧;
  • 脚本以 2 退出:强制拦截。以 PreToolUse 为例,这次工具调用不会执行,而且 stderr 里的拦截理由会被喂给 Claude——它下一轮就带着这个理由去改。

比如你想禁止在主仓库直接跑 git push --force,一个几行的脚本就够了:发现命令匹配就 exit 2,顺便 echo 一句“强推请走 PR”。Claude 收到后不会傻站着,它会换一条路(比如开个分支再推)。拦截变成了引导,这正是 Hooks 比“删掉它的 Bash 权限”高明的地方——前者教会它规矩,后者只是砍掉手脚。

一次工具调用,要过两道关卡 Claude 发起工具调用 · 比如跑个测试 关卡一 · PreToolUse:你的脚本先过目 工具真正执行 · 过程主对话可见 关卡二 · PostToolUse:干完了再查一道 结果回到 Claude · 继续下一步 关卡是脚本,不是提示词 从 stdin 拿到调用详情,想查什么查什么 退出码说话 0 放行;2 拦下,理由喂给 Claude 让它改道 这就是“过程污染”的来源 主对话里干活,每一步都占上下文 自动格式化挂这里 改完文件自动跑 prettier,落盘即规整 想捎话给 Claude? 走退出码 2 或 JSON 字段,别 print 到 stdout 提示词是贴在墙上的规范,关卡是流水线上的质检员

实战:一套让 AI 干活有声音的配置

机制讲完了,看一个我实际在用的完整系统。它解决的不是代码问题,而是一个更日常的痛点:让 AI 干活时你不用盯屏幕。

Claude Code 跑长任务时,你切去干别的,回来一看——它五分钟前就停了,在等你确认。或者更糟:它弹了个权限请求,安安静静等着,你以为它还在干活。屏幕盯久了累,不盯又怕耽误事。

解法是把“眼睛”换成“耳朵”:给关键事件配上音效。这套配置在我的项目里长这样:

{
  "hooks": {
    "Stop": [
      { "hooks": [ { "type": "command", "command": "python3 .claude/hooks/scripts/hooks.py" } ] }
    ],
    "PreToolUse": [
      { "hooks": [ { "type": "command", "command": "python3 .claude/hooks/scripts/hooks.py" } ] }
    ]
  }
}

所有事件都指向同一个 Python 脚本,由它分发。脚本干三件事:认出这次是哪个事件、查一下这个事件有没有被禁用、然后播放对应的提示音。

听声音干活:事件到音效的分发 所有事件指向同一个脚本,由它分发 事件到达 · Stop、PreToolUse、要权限…… hooks.py 认事件 · git commit 有专属音 查开关 · hooks-config.json 一行一个事件 开着 · 播放 sounds/ 里对应的音效 关着 · 静默跳过,零打扰 完成、要权限、子代理回来了,各是各的音 嫌吵的事件,个人在 local 配置里关掉 眼睛换耳朵:AI 的状态,听得见 跑长任务、派分身时,不用再盯屏幕等它

它有几个设计细节值得说:

事件各配各的音。 任务完成是一个音、要权限是另一个音、子代理回来了又是一个。用一阵子之后,你闭着眼就知道当前进展到哪一步——听到“权限请求”的音再起身,一点不耽误。

git commit 有专属提示音。 脚本里对 PreToolUse 做了特殊处理:发现这次调用是 git commit,播放一段专门的音效。这个设计的妙处在于利用了 matcher 给不了的信息——matcher 只能按工具名匹配,分不出“这次 Bash 是跑测试还是提交代码”,脚本读 stdin 里的命令内容再分。

开关独立成配置文件。 30 来个事件,不是每个都想听。全部开关收在 hooks-config.json 里,一行一个 disableXxxHook。团队共享这份基础配置,个人嫌吵的,在自己的 hooks-config.local.json 里覆盖(这份文件进 .gitignore)——和第 2 篇讲的配置分层是同一个思想:共享的归共享,私人的归私人。

这套东西的成本是一百来行 Python,收益是“AI 的状态我能听见了”。跑后台任务、派子代理的场景下,尤其值。

更多玩法:从自动格式化到危险命令拦截

同一套机制,换个脚本就是另一个工具。给两个最常见的方向:

自动格式化。在 PostToolUse 上挂一个脚本,匹配 Edit|Write 工具,每次 Claude 改完文件就自动跑一遍 prettier 或 gofmt。Claude 写得再野,落盘的代码都是格式化过的——这条规矩从此不需要它在对话里“记得”。

危险命令拦截。第 3 篇提过 /careful 技能:一调用就拦截 rm -rf、强推这类命令。用 PreToolUse 写几行脚本就能做到,退出码给 2,理由写明白。这种检查属于典型的“能自动化的规矩别写成文字”——第 2 篇说过的原则,Hooks 就是它的实现手段。

几个容易踩的坑

坑一:Hook 慢,每次操作都在还债。 PreToolUse 是同步的——你的脚本跑完之前,工具调用就一直在等。挂个要跑十秒的检查,等于给每次工具调用加十秒延迟。重检查用 async: true 让它异步跑,或者挪到 PostToolUse、Stop 这类不挡路的时机。

坑二:想给 AI 捎话,出口没找对。 脚本 exit 0 时,stdout 的普通输出默认只进调试日志,Claude 看不见。想让 Claude 看到你的提示,要么用 2 退出码(拦下并说明理由),要么按官方的 JSON 输出格式给 additionalContext 之类的字段。我就见过有人往 stdout print 了一大段“注意事项”,纳闷为什么 AI 毫无反应——输出到哪儿,比输出了什么更重要。

坑三:别把 Hooks 当提示词的复读机。 每个事件都触发一次脚本,挂多了配置本身就变成负担。只挂“程序才能保证”的事:格式化、拦截、通知、记录。判断标准还是那句:harness 能强制的事,就别劳烦提示词;而 Hooks,是你在自己动手造 harness。

收工清单

  1. 提示词是建议,Hooks 是制度:需要“保证发生”的事,上 Hooks
  2. 三层结构:事件 → matcher → 命令;脚本从 stdin 拿 JSON,exit 2 拦截并把理由喂给 Claude
  3. 事件数量随版本增长,别背清单,用前查官方文档
  4. 同步脚本要快,重活用 async 或挪到不挡路的事件
  5. 想让 Claude 看到信息,走退出码 2 或 JSON 字段,别 print 到 stdout
  6. 通知类需求(提示音、桌面提醒)从 Stop 和 PermissionRequest 挂起,收益立竿见影
  7. 开关集中放一个配置文件,个人覆盖用 local 层,团队共享不打架

下一篇,我们补上这套体系里最后一块地基:settings.json——前面几篇反复提到的权限、模型、Hooks 挂载点,全都在这一个文件里。搞懂它的六层优先级,你才算真正拿到了这套系统的控制台。

系列导航