Hook以当前用户的权限自动运行任意命令,不会逐次向我们确认。
一条配置错误或恶意的Hook足以删除文件、外泄数据。
启用任何来路不明的Hook配置前,务必逐条审阅其命令内容。
引子
在工程里有一类事必须要做(例如:改完代码必须格式化、rm -rf必须拦下、测试没过不许收工),我们把这类硬规则写进CLAUDE.md,本质上仍是"建议",靠的是模型的"自觉"。
那么,怎样让一项任务百分之百被执行,而不靠模型自觉?
Hook(钩子)
原理
理解Hook,有四个要点:
- 凭什么是确定的?
- 挂在循环的哪个节点?
- 配置长什么样?
- 哪几个事件最常用?
确定性机制
Hook的"确定性",靠的是绕开模型,进入到工具调用循环中。
前几章的CLAUDE.md、Skill(技能)、Sub-Agent(子智能体)都作用于模型的输入,要不要照做,最终由模型的概率环节决定,大概率遵守,但没有机制保证百分之百。
Hook不同,它是一段挂在循环节点上的代码:事件一到,系统直接执行,模型没有投票权。
运行机制
运行机制:工具调用循环(agent loop)的每个阶段都有固定的节点,Hook就挂在这些节点上。
配置结构
Hook配置是标准JSON,写在settings文件的hooks键下,按事件名分组。
每个事件对应一个数组,数组元素由两部分组成
matcher限定这组Hook的适用范围hooks是动作列表
示例代码:
1 | { |
事件
什么是事件
类比前端里的事件(click、touch),我们能更好的理解ClaudeCode中的事件。
在前端,click是"用户点了一下"这个时刻,onclick是你挂在这个时刻上的处理器,点击一发生,处理器就自动执行。touch与ontouch同理。
而Hook只是把监听对象从"页面上的用户动作"换成了"工具调用循环里的节点",这就是事件。
按粒度,事件分三层:
- 会话级
整个会话触发一次,如会话启动、会话结束。 - 轮级
每轮对话触发一次,如用户提交prompt、Claude完成响应。 - 工具级
每次工具调用触发一次,如工具执行前、工具执行后。
五个高频事件
在约三十个事件里,下面五个覆盖了绝大多数场景。
PreToolUse
工具执行前触发,整个体系里最常用的拦截点。
它可以放行、拒绝、转交用户确认,还可以改写工具参数后再执行。
作用是拦截危险命令、保护敏感文件都在这里做。PostToolUse
工具成功执行后触发。
此刻操作已经发生、无法撤销,能做的是补偿与反馈:自动格式化刚写入的文件、记录审计日志,或把问题反馈给Claude要求它处理。UserPromptSubmit
用户提交prompt后、Claude开始处理前触发。
可以拦下这轮输入,也可以向这轮对话注入信息。Stop
Claude认为本轮工作完成、准备停下时触发。
可以阻止它收工,并把原因反馈给它继续干活。
例如,"测试没过不许结束"这类质量闸门就挂在这里。SessionStart
会话开始或恢复时触发,matcher可区分startup/resume/clear/compact四种来源。
适合在会话起点注入环境信息,或在上下文压缩后重新注入提醒。
比较
- 希望模型遵守的约定,写CLAUDE.md。
- 能事先划清的边界,交给权限规则。
- 必须临场判断、必然执行的动作,挂Hook。
| 层 | 性质 | 怎么生效 |
|---|---|---|
| CLAUDE.md | 建议 | 写进模型的输入,模型大概率听。 |
| 权限 | 静态策略 | 用allow/deny/ask规则事先划清边界。 |
| Hook | 动态响应 | 在事件发生的瞬间用代码做决定。 |
Hook与权限之间有一条明确的分界,例如PreToolUse返回allow可以跳过交互式确认,但不能越过权限规则里的deny。
示例
需求
假设存在一个项目,团队约定:
- 破坏性命令必须拦截
- 写完文件立即格式化
- 测试没过不许收工
- 上下文压缩后要重申规范
我们把四段配置都放在项目的.claude/settings.json,可以合并进同一个hooks键。
破坏性命令必须拦截
基于PreToolUse拦截危险命令。
- matcher限定
Bash。 - 脚本用
jq取出即将执行的命令,发现rm -rf就以exit 2阻止。 - stderr里的原因会反馈给Claude。
示例代码:
1 | { |
写完文件立即格式化
基于PostToolUse自动格式化。
- matcher写
Edit|Write - 文件一写完就交给Prettier
这样的话,Claude不需要知道、也不需要记得项目用什么格式化规范,这件事从"模型的义务"变成了"系统的性质"。
示例代码:
1 | { |
测试没过不许收工
基于Stop控制测试闸门。
示例代码:
1 | { |
解释说明:Stop事件不支持matcher,所以上面的配置条目里只有hooks、没有matcher。
上下文压缩后要重申规范
基于SessionStart压缩后提醒。
- matcher取
compact,只在上下文压缩后的新起点触发,把最容易在压缩中丢失的约定重新交代一遍。
示例代码:
1 | { |
实践
事件清单
事件总数约三十个,且随版本时有增删,这里不逐一罗列,完整、权威的清单以官方文档为准:
查阅时按三层结构(会话级、轮级、工具级,见前文「运行机制」)去定位,重点看三件事:何时触发、可否阻止、matcher取什么值。
配置位置
Hook可以配置在六个位置。
其中四个就是settings文件的标准层级(~/.claude/settings.json、.claude/settings.json、.claude/settings.local.json与Managed设置)
另有两个:
- Plugin的
hooks/hooks.json
Plugin启用期间生效,随插件分发。 - Skill与Agent的YAML front matter
组件激活期间生效,随组件定义分发。
注意:多个位置的Hook是叠加关系而非覆盖关系。
例如,Plugin、Skill与settings文件中挂在同一事件上的Hook会重合执行。
matcher写法
语法规则
| 写法 | 语义 | 例子 |
|---|---|---|
| 精确工具名 | 大小写敏感的全名匹配 | Bash、Edit |
| 管道分隔 | 多工具"或" | Edit\|Write、Edit,Write |
| 正则 | 按正则匹配工具名 | mcp__github__.* |
*或空字符串 |
匹配全部工具 | * |
| 事件专用值 | 非工具事件各有取值 | SessionStart的compact、Notification的permission_prompt |
注意:
- matcher大小写敏感,
bash匹配不到Bash工具,且写错不会报错、只是静默不触发 FileChanged的matcher按文件名字面值匹配,写法如.envrc\|.env,其中的管道符需要转义。
例子
例如,用mcp__github__.*一次覆盖某个MCP服务的所有工具。
示例代码:
1 | { |
五种Hook类型
语法规则
Hook类型共五种:
command,执行一条shell命令或脚本。http,把事件发送到一个HTTP端点。prompt,把当前情况交给一个小模型做单次评估,默认用Haiku,可用model字段覆盖。适合规则写不死、但一眼能判断的检查。agent,实验特性。启动一个能调用工具的Agent做多轮核查,最多50个工具轮次,成本与延迟也最高。mcp_tool,直接把MCP服务上的某个工具当Hook执行。
例子
示例代码:
1 | { |
解释说明:
command执行本地脚本。http把"事件的输入JSON"POST到url。prompt与agent的$ARGUMENTS会被替换成"事件的输入JSON",然后交给模型判断。mcp_tool调用server上名为tool的工具。- 我们把五种Hook挂在同一事件上,这五种都会被执行,但顺序不确定。
输入输出协议
概述
- 输入走stdin
- 输出走exit code和stdout
输入:stdin
事件触发时,Hook从stdin收到一份JSON,通用字段如下。
示例代码:
1 | { |
输出:exit code
- 只有
command类型以exit code表达意图。 exit 2的设计把"有意阻止"与"脚本坏了"分开:脚本自身的故障,不应该拦下正常的工作流。
| exit code | 行为 |
|---|---|
| 0 | 成功。若stdout是JSON则解析为决策,否则继续正常流程 |
| 2 | 阻止。对可阻止事件生效,stderr内容反馈给Claude;对不可阻止事件仅显示stderr |
| 其他 | 视为脚本自身故障,流程继续。stderr首行连同Hook错误通知显示,完整输出记入调试日志 |
输出:JSON
语法规则
- exit code是粗粒度的,细粒度控制用exit 0加stdout JSON。
- exit 2与JSON输出不可混用,一旦以exit 2退出,stdout里的JSON就会被忽略。
| 字段 | 说明 |
|---|---|
continue |
设为false时Claude停止处理,配合stopReason给出原因 |
decision |
取值"block",用于Stop/PostToolUse/UserPromptSubmit,配合reason说明缘由 |
additionalContext |
注入Claude上下文的额外信息 |
systemMessage |
显示给用户的警示文本 |
suppressOutput |
隐藏stdout |
terminalSequence |
向终端发送转义序列,可用于通知、改标题 |
hookSpecificOutput |
事件特定的控制 |
例子
上文测试闸门用exit 2实现,等价的JSON写法是:
1 | { |
若不想阻止、只想补充信息,用additionalContext把内容注入Claude的上下文,工作流照常继续:
1 | { |
一事件挂了多个Hook
当同一事件挂了多个Hook时,规则如下:
- 最严决策胜出
按deny>defer>ask>allow取最严格的决策。
例如同一个Bash挂了两个PreToolUseHook,白名单脚本判定放行(allow),安全扫描却发现危险命令要求拒绝(deny),取最严,最终按deny执行、命令被拦下。 additionalContext拼接
所有匹配Hook返回的additionalContext会拼成一段一起交给Claude,而非相互覆盖。
例如SessionStart挂了两个Hook,一个注入当前分支与最近提交,另一个注入团队编码规范,两段会拼接后进入上下文。- 执行顺序不确定
同一事件的多个Hook执行顺序不定。