10 · 怎么让操作 100% 执行
翔宇有一条原则:凡是需要人类记住的事情,都应该让机器来做。反过来,凡是需要 AI「记住」去做的事情,都应该让代码来强制。这不是对 AI 不信任,是工程纪律。Ho
翔宇有一条原则:凡是需要人类记住的事情,都应该让机器来做。反过来,凡是需要 AI「记住」去做的事情,都应该让代码来强制。这不是对 AI 不信任,是工程纪律。Hooks 就是这条原则的产物。——翔宇
要点速览
- 你将理解 AI 的灵活性和脚本的确定性各自的边界
- 你将理解 Hooks 如何在两者的交界处找到平衡
- 你将理解 Hook 事件的设计逻辑——为什么在这些时间点插入自动化
- 你将理解退出码 2 的特殊含义和它背后的通信机制
- 你将理解四种 Hook 类型如何在「确定触发」的框架里引入不同级别的智能
1. 两个极端
先把两样东西摆在一起看。
左边是 AI。 你在 CLAUDE.md 里写:「每次编辑 TypeScript 文件后,用 Prettier 格式化。」Claude Code 读到了,理解了。大多数时候它也照做了。但偶尔——大概每十次里有一两次——它忘了。不是它不识字,是在那一次特定的上下文里,注意力被其他事情占据了。
AI 的本质是概率系统——它根据上下文预测下一步最可能的动作。大概率正确,但无法保证每次都正确。
右边是脚本。 你写一个 Shell 脚本:每次文件改动后跑 Prettier。它不理解你为什么要格式化,不理解 TypeScript 是什么,不理解项目的上下文。但你让它跑,它就跑。100 次里 100 次都跑。
脚本是确定性系统——给定输入,输出永远一样。但它不灵活,无法根据情况变通。
🧠 底层逻辑 这两者的差异不是程度差异,是本质差异。AI 是「大概率正确」,脚本是「确定性正确」。对于「代码风格」这种容错度高的场景,概率性够用了。但如果场景换一下呢——你在 CLAUDE.md 里写:「永远不要修改 .env 文件。」AI 遵循这条规则的概率是 99%?即便如此,那个 1% 的时刻,你的数据库密码可能被覆盖。有些事情,99% 和 100% 的差距是无限大的。
2. 并排照镜子:CLAUDE.md vs Hooks
现在正式把两种机制放在一起对比。
CLAUDE.md 说「请用 Prettier 格式化」,Claude Code 会尽力做到——但它是「建议」,Claude Code 可以因为种种原因跳过。就像公司有一条规定:「建议员工每天写工作日志。」有人写了,有人没写,没人受罚。
配置一个 Hook:每当 Claude Code 编辑完文件,系统自动运行 Prettier。就像公司装了门禁系统:不刷卡就进不了门。不是「建议你刷卡」,是物理上进不去。
| 维度 | CLAUDE.md | Hooks |
|---|---|---|
| 类比 | 工作建议 | 门禁系统 |
| 执行保障 | 概率性(AI 自主判断) | 确定性(代码强制执行) |
| 灵活性 | 高——AI 可以根据上下文变通 | 低——不管什么情况都执行 |
| 适合什么 | 风格偏好、编码规范、工作流提示 | 格式化、安全防护、日志审计、通知 |
| 核心语义 | should-do(应该做) | must-do(必须做) |
🎯 一句话理解 选哪个不取决于技术,取决于你能不能容忍偶尔的遗漏。能容忍 → CLAUDE.md。不能容忍 → Hooks。
这里有一个容易掉进去的坑。有人一看到 Hooks 比 CLAUDE.md「更可靠」,就想把所有规则都做成 Hooks。
不要这样。
想想门禁的比方——如果公司里每一扇门都装了门禁,每走一步都要刷卡,你会崩溃的。门禁只装在关键入口:大门、机房、财务室。其他地方用信任。
同理,Hooks 只用在必须 100% 执行的场景。其他的留给 CLAUDE.md 的灵活性。
3. 关键区别在哪——时间线上的检查站
现在深入 Hooks 这一侧。
Hook 是在 Claude Code 生命周期的特定时间点自动执行的脚本。
「生命周期的特定时间点」——这句话里每个字都重要。想象 Claude Code 在工作的过程像一条时间线。它启动、读你的指令、思考、调用工具(读文件、改代码、跑命令)、给你回复、结束。
🎨 打个比方 工厂的流水线上有质检工位。零件经过时,质检员检查一下——合格放行,不合格打回。质检员不需要知道这条流水线在生产什么,它只管在自己的工位上做检查。Hook 就是流水线上的质检工位:你决定在哪个位置设工位,你决定检查什么条件,系统保证每个零件都经过检查。
Claude Code 提供了十几个事件点。看起来很多,但逻辑很清晰——分成四组。
a. 工具执行前后(最常用)
PreToolUse——工具执行前。最经典的用法:阻止危险操作。Claude Code 准备写一个文件,你的 Hook 检查一下这个文件是不是 .env——是的话,阻止;不是的话,放行。
PostToolUse——工具执行后。最经典的用法:自动格式化。Claude Code 刚改完一个 TypeScript 文件,你的 Hook 自动跑 Prettier。
PermissionRequest——权限弹窗时。某些你信任的操作(如 npm run lint)可以自动放行,免得每次手动点。
b. 用户交互
UserPromptSubmit——你按下回车的瞬间。可以在 Claude Code 看到你的提示之前注入额外上下文、验证内容。
Notification——Claude Code 要发通知的时候。你可以自定义通知方式——弹桌面提醒、播放声音、发到 Slack。
c. 会话生命周期
SessionStart——会话开始时。适合注入环境信息:当前时间、Git 分支、项目配置。
SessionEnd——会话结束时。适合清理工作:记录统计信息、保存日志。
PreCompact / PostCompact——上下文压缩前后。PostCompact 可以在压缩后注入关键上下文,确保重要信息不因压缩而丢失。
d. 停止与配置
Stop / SubagentStop——Claude Code 完成回复、准备停下来的时候。你可以检查任务是不是真的完成了——如果没完成,阻止它停下来,让它继续。
Elicitation / ElicitationResult——和第 9 篇讲的 MCP Elicitation 配合使用。当 MCP 服务器向用户请求输入时触发,你可以拦截或审计这些请求。
4. 退出码 2——一个精巧的通信机制
回到对比视角。脚本的世界里,进程之间怎么通信?
答案是通过退出码(exit code)。进程退出时返回一个数字,调用方根据这个数字决定下一步。这是 Unix 系统级的通信方式。
| 退出码 | 含义 | Claude Code 的反应 |
|---|---|---|
| 0 | 成功,一切正常 | 继续执行 |
| 2 | 阻止这个操作 | 取消工具调用,把你的错误信息反馈给 AI |
| 其他(1、3、4……) | 出错了但不阻止 | 显示错误信息,但操作照常进行 |
注意退出码 2 的特殊之处。它不只是「阻止操作」——它还会把你写到 stderr(标准错误输出)里的内容反馈给 Claude Code。Claude Code 不只是被阻止了,它还知道为什么被阻止。
🔍 深入一步 为什么不是退出码 1?因为退出码 1 在 Unix 世界里太常见了——几乎任何脚本出错都返回 1。如果用 1 来表示「阻止」,那脚本里一个无关的语法错误也会触发阻止,这不是你想要的。退出码 2 是一个刻意的选择:它不太可能被意外触发,所以你可以放心地把它当作明确的「阻止」信号。这种设计叫「带外信令」——用一个不太常见的值来传递特殊含义,避免和普通情况混淆。
举个具体的流程。你的 PreToolUse Hook 检查到 Claude Code 要写 .env 文件:Hook 返回退出码 2 → 同时在 stderr 里写「.env 文件包含敏感信息,禁止修改」→ Claude Code 看到退出码 2,取消写操作 → Claude Code 读到 stderr 的内容,理解了原因 → 调整策略,可能会告诉你「我无法修改 .env 文件,你需要手动更新」。
整个过程没有任何人工干预。Hook 自动触发、自动阻止、自动解释。
5. 什么时候用哪个——阻止时机的选择
这里有一个设计决策直接关系到 AI 灵活性和确定性的平衡。
你想在什么时候阻止一个操作?是每次写入的时候立刻检查?还是等 Claude Code 做完所有事情、最后提交的时候统一检查?
💬 通俗讲 你在写一篇论文。有两种审核方式:一种是你每写一段就被打断——「这里格式不对,改」;另一种是你写完整篇,最后统一审核。前者精确但打断思路,后者流畅但可能最后改动量大。
如果你在 PreToolUse 上设了很多 Hook,Claude Code 每次操作前都被检查一遍——频繁的阻止和打断可能让它的工作流变得支离破碎。
官方的建议是:优先 Block-at-Submit。让 Claude Code 完成整个计划,最后在 commit 的时候统一用 Hook 检查。只在真正危险的场景下使用 Block-at-Write——比如阻止修改 .env 文件,这种操作一旦发生就不可逆,不能等到最后再检查。
6. 更深的统一:四种 Hook 类型
到这里你可能觉得 Hooks 就是执行 Shell 命令。不止。
Hook 有四种类型,每种在「确定性-灵活性」光谱上的位置不同:
command 类型——执行 Shell 命令。最常见的,也是执行速度最快的。适合确定性规则:格式化、文件保护、日志记录。光谱上完全偏向确定性。
prompt 类型——调用 LLM 做判断(默认用 Haiku 模型)。触发是确定性的(每次到这个事件点都触发),但判断是智能的(LLM 分析上下文后决定放行还是阻止)。适合规则太复杂、写不成代码的场景——比如判断一个任务是不是真的完成了。光谱上在中间。
agent 类型——生成子代理,可以使用 Read、Grep、Glob 等工具验证条件后返回决策。比 prompt 类型更强大,能在判断前先读取文件确认状态。光谱上偏向灵活性。
http 类型——发 HTTP 请求到外部服务。适合和 Webhook、审计系统、通知平台集成。光谱上取决于外部服务的逻辑。
🔑 关键点 prompt 和 agent 类型是这个系统最精妙的部分。它们在确定性的框架里引入了智能判断——触发是 100% 确定的,但触发后做什么决定,由 AI 根据上下文判断。这是「确定性触发 + 智能决策」的组合,既保证不会漏掉,又不至于死板。
四种类型不是替代关系,是光谱上的不同位置。根据你对确定性的需求程度选择。
7. Hook 的配置归属
Hook 配置放在哪里,决定了它的生效范围。这个逻辑和第 9 篇讲的 MCP 配置范围一模一样。
| 层级 | 文件位置 | 谁能看到 | 适合什么 |
|---|---|---|---|
| 用户级 | ~/.claude/settings.json | 你自己,所有项目 | 个人习惯(通知方式、日志偏好) |
| 项目级(共享) | .claude/settings.json | 整个团队 | 团队规范(代码格式化、安全规则) |
| 项目级(个人) | .claude/settings.local.json | 你自己,当前项目 | 本地调试 |
个人用的放全局,团队用的放项目,调试用的放本地。
8. 幂等性——一个容易忽视的工程细节
不管你选择哪种 Hook,有一个技术细节必须点一下。
Hook 可能被多次触发——Claude Code 编辑了同一个文件三次,PostToolUse Hook 就跑了三次。你的脚本必须能安全地重复执行而不产生副作用。这叫幂等性。
幂等性就是「跑一次和跑一百次结果一样」。比如日志用追加(>>)不用覆盖(>),格式化操作本身就是幂等的(已经格式化过的文件再格式化一次不会变)。如果你的 Hook 是「每次触发往数据库插一条记录」,那就不是幂等的——跑三次会插三条重复记录。设计 Hook 时这个点要想清楚。
9. 连接前面的概念——三角关系
到这里,你可能注意到了一个有趣的三角关系:
MCP(第 9 篇)解决的是「能做什么」——can-do。给 Claude Code 接上更多能力。
CLAUDE.md(第 3 篇)解决的是「应该怎么做」——should-do。给 Claude Code 方向和偏好。
Hooks(这篇)解决的是「必须怎么做」——must-do。在关键节点上强制执行规则。
三者合在一起,构成了控制 AI 行为的完整光谱。从最松的「建议」到最紧的「强制」,中间还有「能力」作为基础。好的 Claude Code 使用者不是只用其中一种,而是在不同场景下选择合适的控制力度——有些事情口头嘱咐就行,有些事情要写进制度,有些事情要给工具支持。三种手段配合使用,才是完整的管理。
⚡ 速记 Hooks = 确定性自动化。四种类型:command(执行命令)、prompt(LLM 判断)、agent(子代理验证)、http(外部请求)。退出码 2 = 阻止操作。优先 Block-at-Submit,只在危险操作时用 Block-at-Write。和 CLAUDE.md 配合使用,不是替代关系。
10. 你真的懂了吗
这篇拆了一个概念:确定性。
- 有人说「我在 CLAUDE.md 里写得很清楚,Claude Code 一定会照做」。你能解释为什么这个信心可能有问题吗?
- 退出码 2 和退出码 1 有什么区别?为什么不用更直觉的退出码 1 来表示阻止?
- Prompt-based Hook 听起来矛盾——Hook 的核心是确定性,但 prompt 的输出是概率性的。这个矛盾是怎么被解决的?
- 你现在的工作流里,有哪些操作适合从 CLAUDE.md 迁移到 Hooks?判断标准是什么?
- Hooks、CLAUDE.md、MCP 分别解决什么层次的问题?用一个现实世界的管理类比能说清楚吗?