八个 Hook,让 AI Agent 从「大概会执行」变成「必然执行」
CLAUDE.md 里的规则大概有 80% 的时候会被遵守。Hook 是 100%。经过六个月的实测,这八个 Hook 一次都没有被我从配置里删掉。
CLAUDE.md 本质上是一份建议文档。你写”提交前跑 Prettier”,Agent 大概有三分之一的概率会跳过这一步。写得更精确也没用。80% 的遵守率不是模型质量问题,而是把规则塞进上下文窗口、然后祈祷模型照做这种方式本身的结构性缺陷。
Hook 的运作机制完全不同。它是在特定生命周期节点自动触发的脚本,和模型的决策无关。PreToolUse 在 Agent 修改文件或运行命令之前触发,PostToolUse 在之后触发。模型没有选择要不要运行它的权利,Hook 就是会跑。
实际差异立竿见影。往 CLAUDE.md 加十条规则,和往 .claude/settings.json 加一个 Hook,是两种完全不同性质的干预。退出码 2 直接阻断 Agent 的操作,退出码 0 放行,其他退出码记录警告但不阻断。Hook 存在 settings.json 里,提交一次,全组成员通过 git 就能同步。
执行前的四道拦截
我跑 Hook 超过六个月了,以下四个 PreToolUse Hook 在每个项目里都活下来了,一次都没被删。
拦截危险命令:通过正则匹配捕获 rm -rf、git reset --hard、DROP TABLE 等破坏性模式,返回退出码 2,在操作发生之前直接终止。我亲眼见过 Agent 尝试对不该碰的目录执行 rm -rf,没有这个 Hook,损失是真实的。
保护敏感文件:屏蔽任何对 .env、package-lock.json、*.pem 等文件的修改尝试。Agent 永远没机会覆盖你的 lock 文件,也不会把凭证泄露进提交记录。
PR 前强制通过测试:将 mcp__github__create_pull_request 设为匹配目标,Agent 在测试通过之前就是无法开 PR,字面意义上的无法。不会再有”后续 PR 里补测试”这种事。
记录每一条命令:把 Agent 执行的每条 bash 命令连同时间戳写入 .claude/command-log.txt。三天后发现某个地方不对劲,可以精确还原当时发生了什么。
执行后的三道检查
PostToolUse Hook 在 Agent 修改文件后立即触发,我把三个 Hook 串联在一起。
自动格式化:对每个被改动的文件跑 Prettier。Python 项目换成 Black,Go 项目换成 gofmt。不管 Agent 有没有记得格式化,格式化都会发生。
自动 Lint:紧跟格式化之后跑 ESLint。ESLint 发现错误,Agent 在同一轮次里就能看到并修复。能进入人工代码审查环节的 lint 问题数量降到接近零。
自动测试:每次文件变更后跑相关测试套件。测试失败,Agent 几秒内就知道,并尝试修复。输出通过 tail -5 只保留摘要部分,防止测试输出把上下文窗口淹掉。
顺序很重要:先 Prettier,再 ESLint,再测试。人看到代码的时候,格式和 lint 都已经过了。代码审查里关于代码风格的评论基本上消失了。
让工作成果不因意外中断而丢失
Stop 阶段有一个 Hook 处理这件事:自动提交。每次 Agent 完成一个响应,就执行 git add -A && git commit。每个工作单元对应一个提交,两个任务的改动永远不会混进同一个提交。
配合 git worktree 使用,可以在功能分支上实现自动的按分支提交。Agent 崩溃或者被中断,最后一段工作成果不会丢失。
踩过的坑
Hook 链式调用听起来很优雅,但调试一条失败的链比调试单个脚本要麻烦得多。有一次自动测试 Hook 开始静默失败,原因是新项目里没有安装测试运行器。我花了一个小时才查清楚为什么 Agent 一直在生产没经过测试的代码。Hook 返回的是退出码 0,因为测试脚本根本不存在,shell 把”命令未找到”当成了非阻断条件。后来我加了一个显式检查,确认测试运行器存在之后才调用它。
另一个约束是性能。常见的担忧是 Hook 多了会拖慢速度,但问题不在数量,在于单个 Hook 能不能在 200 毫秒以内完成。单文件的 Prettier 大约跑 50 毫秒,ESLint 检查大约 80 毫秒。测试耗时因项目而异,但限定在受影响的文件范围内,大多数情况都跑得快。某个 Hook 单次超过一秒,Agent 的反馈循环就开始变得迟钝。
这套机制背后的共同逻辑
OpenAI 的 Harness Engineering 博客提到,Agent 在边界清晰、结构可预期的环境里表现最好。React 的设计哲学对组件说的是同一件事:可组合的单元,加上定义好的生命周期阶段和状态。
Claude Code 的 Hook 遵循同样的抽象。状态对应 Session 和 Memory,Hook 是在生命周期边界介入的函数。PreToolUse 设定边界,PostToolUse 让结构可预期,Stop 保存结果。
我以前放在 CLAUDE.md 里的”跑 Prettier”那行已经删了。Hook 每次都会跑,不需要任何人去要求它。
订阅通讯
获取最新 AI 洞见。