简介
Claude Code的本质是"大模型+工具调用循环+有限上下文窗口"。前几章给出的手段——CLAUDE.md持久注入、Skill(技能)按需注入、Sub-Agent(子智能体)隔离执行——有一个共同点:都作用于模型的输入,最终都要经过大模型这个概率环节,模型大概率会遵守,但没有任何机制保证百分之百。《4.SubAgent》结尾留下的正是这个问题:委派做得再精细,也不能保证某件事必然发生。
而工程里恰恰有一类事必须必然发生:改完代码必须格式化、rm -rf必须拦下、测试没过不许收工。这类硬规则写进CLAUDE.md,本质上仍是"建议"。Hooks(钩子)是Claude Code为这类需求提供的确定性机制:把我们定义的脚本、HTTP端点或提示挂在生命周期的特定事件上,事件一到就执行,不经过模型判断,也不受上下文长短影响。
本章以CLI形态完整展开Hooks,讲清楚四件事:事件有哪些、Hook有哪几种类型、matcher怎么写、输入输出协议是什么。
Hook以当前用户的权限自动运行任意命令,不会逐次向我们确认。一条配置错误或恶意的Hook足以删除文件、外泄数据。启用任何来路不明的Hook配置前,务必逐条审阅其命令内容。
运行模型与配置位置
Hook的运行模型一句话可以说清:工具调用循环(agent loop)的每个阶段都有固定的节点,Hook就挂在这些节点上,循环本身的机制见《1.概述》。按粒度,事件分三层。
- 会话级
整个会话触发一次,如会话启动、会话结束。 - 轮级
每轮对话触发一次,如用户提交prompt、Claude完成响应。 - 工具级
每次工具调用触发一次,如工具执行前、工具执行后。
配置结构
Hooks配置是标准JSON,写在settings文件的hooks键下,按事件名分组。每个事件对应一个数组,数组元素由两部分组成:matcher限定这组Hook的适用范围,hooks是处理器列表。
示例代码:
1 | { |
配置位置
Hooks可以配置在六个位置。
| 位置 | 作用域 | 可否共享 |
|---|---|---|
~/.claude/settings.json |
本机所有项目 | 否,只在本地机器 |
.claude/settings.json |
单个项目 | 是,可提交进版本库 |
.claude/settings.local.json |
单个项目 | 否,通常被.gitignore忽略 |
| Managed设置 | 组织范围 | 是,管理员统一控制 |
插件的hooks/hooks.json |
插件启用期间 | 是,随插件分发,打包方式见《9.Plugins》 |
| Skill与Agent的front matter | 组件激活期间 | 是,随组件定义分发 |
前四行就是settings文件的标准层级,各层路径与优先级结论见《1.概述》。Skill可以在front matter里声明hooks字段、随Skill激活而生效,见《3.Skills》;Sub-Agent同理,见《4.SubAgent》。
注意:多个位置的Hooks是叠加关系而非覆盖关系,插件、Skill与settings文件中挂在同一事件上的Hook会重合执行。
事件清单
事件总数约三十个,不必背,我们按三层结构列出主干事件,精讲其中五个高频事件,其余列在专项表里供查阅。
会话级事件,每个会话触发一次:
| 事件 | 触发时刻 | 可否阻止 | matcher |
|---|---|---|---|
SessionStart |
会话开始或恢复 | 否 | startup/resume/clear/compact |
SessionEnd |
会话终止 | 否 | clear/resume/logout/prompt_input_exit/bypass_permissions_disabled/other |
Setup |
--init-only或CI初始化 |
否 | init/maintenance |
轮级事件,每轮对话触发一次:
| 事件 | 触发时刻 | 可否阻止 | matcher |
|---|---|---|---|
UserPromptSubmit |
提交prompt后、Claude处理前 | 是,exit 2或JSON decision: "block" |
无 |
Stop |
Claude完成一轮响应 | 是,exit 2或JSON decision: "block" |
无 |
StopFailure |
因API错误结束一轮 | 否,输出与exit code被忽略 | 错误类型 |
工具级事件,每次工具调用触发一次:
| 事件 | 触发时刻 | 可否阻止 | matcher |
|---|---|---|---|
PreToolUse |
工具执行前 | 是,exit 2或JSON deny |
工具名 |
PermissionRequest |
权限对话显示前 | 是,JSON decision.behavior |
工具名 |
PermissionDenied |
auto模式拒绝工具后 |
否,可返回retry: true |
工具名 |
PostToolUse |
工具成功执行后 | 是,JSON decision: "block" |
工具名 |
PostToolUseFailure |
工具执行失败后 | 否 | 工具名 |
PostToolBatch |
并行工具批次完成后 | 是,JSON decision: "block" |
无 |
专项事件,只列名供查阅:
| 事件 | matcher |
|---|---|
Notification |
permission_prompt/idle_prompt/auth_success/elicitation_* |
SubagentStart、SubagentStop |
Agent类型,general-purpose/Explore/Plan/自定义 |
PreCompact、PostCompact |
manual/auto |
ConfigChange |
user_settings/project_settings/local_settings/policy_settings/skills |
CwdChanged |
无 |
FileChanged |
文件名字面值,管道需转义 |
InstructionsLoaded |
session_start/nested_traversal/path_glob_match/include/compact |
WorktreeCreate、WorktreeRemove |
无 |
MessageDisplay |
无 |
TaskCreated、TaskCompleted |
无 |
TeammateIdle |
无 |
UserPromptExpansion |
Skill或命令名 |
Elicitation、ElicitationResult |
MCP服务器名 |
五个高频事件
PreToolUse
工具执行前触发,整个体系里最常用的拦截点。它可以放行(allow)、拒绝(deny)、转交用户确认(ask),还可以通过updatedInput改写工具参数后再执行。拦截危险命令、保护敏感文件都在这里做。PostToolUse
工具成功执行后触发。此刻操作已经发生、无法撤销,能做的是补偿与反馈:自动格式化刚写入的文件、记录审计日志,或用decision: "block"把问题反馈给Claude要求它处理。UserPromptSubmit
用户提交prompt后、Claude开始处理前触发。可以用exit code 2拦下这轮输入,也可以借additionalContext向这轮对话注入信息。Stop
Claude认为本轮工作完成、准备停下时触发。用decision: "block"可以阻止它收工,并把reason反馈给它继续干活。"测试没过不许结束"这类质量闸门就挂在这里。SessionStart
会话开始或恢复时触发,matcher可区分startup/resume/clear/compact四种来源。适合在会话起点注入环境信息,或在上下文压缩后重新注入提醒。它不能阻止任何事。
Hook类型与matcher
五种类型
type字段决定Hook是什么,共五种:command、http、prompt、agent、mcp_tool。
command是主力类型,执行一条shell命令或脚本。
示例代码:
1 | { |
command
要执行的shell命令或可执行文件。args
可选。一旦提供,命令改以exec形式执行,参数原样传入、不经过shell分词。async
设为true时在后台运行,不阻塞Claude继续工作。asyncRewake
后台运行,且当脚本以exit code 2结束时唤醒Claude。shell
取值bash或powershell。
其余四种类型简述如下。
http
把事件发送到一个HTTP端点,字段有url与headers;headers中要内插的环境变量必须在allowedEnvVars里显式列出。prompt
把当前情况交给一个小模型做单次评估,默认用Haiku,可用model字段覆盖。适合规则写不死、但一眼能判断的检查。agent
实验特性。启动一个能调用工具的Agent做多轮核查,默认超时60秒,最多50个工具轮次,成本与延迟也最高。mcp_tool
直接把MCP服务器上的某个工具当Hook执行,字段为server、tool、input,input中可用${tool_input.file_path}这类模板变量——MCP(Model Context Protocol,模型上下文协议)是什么、服务器怎么接入,下一章回收,见《6.MCP》。
选型从上往下走:能用command写成确定性规则的,不要交给prompt;prompt够用的,不要上agent。确定性与速度沿这个方向递减。
通用字段与超时
五种类型共享一组通用字段。
| 字段 | 说明 |
|---|---|
type |
必填,五种类型之一 |
if |
可选,用权限规则语法进一步过滤,如Bash(git *)只匹配git命令、Edit(*.ts)只匹配对.ts文件的编辑,规则语法见《1.概述》;需Claude Code v2.1.85+ |
timeout |
可选,单位秒,默认值见下表 |
statusMessage |
可选,Hook执行期间显示的状态提示 |
once |
可选,在Skill中声明的Hook只运行一次 |
超时默认值随类型和事件而异:
| 类型 | 默认 | UserPromptSubmit事件下 |
MessageDisplay事件下 |
|---|---|---|---|
command/http/mcp_tool |
600秒 | 30秒 | 10秒 |
prompt |
30秒 | — | — |
agent |
60秒 | — | — |
matcher写法
| 写法 | 语义 | 例子 |
|---|---|---|
| 精确工具名 | 大小写敏感的全名匹配 | Bash、Edit |
| 管道分隔 | 多工具"或" | Edit\|Write,v2.1.191+也可写Edit,Write |
| 正则 | 按正则匹配工具名 | mcp__github__.* |
*或空字符串 |
匹配全部工具 | * |
| 事件专用值 | 非工具事件各有取值 | SessionStart的compact、Notification的permission_prompt |
两个细节。其一,matcher大小写敏感,bash匹配不到Bash工具。其二,FileChanged的matcher按文件名字面值匹配,写法如.envrc\|.env,其中的管道符需要转义。
输入输出协议
Hook与Claude Code之间的通信很朴素:输入走stdin,输出走exit code加stdout。
输入
事件触发时,Hook从stdin收到一份JSON,通用字段如下。
示例代码:
1 | { |
工具级事件额外带tool_name与tool_input两个字段,后者是该工具的完整参数,例如Bash工具的tool_input.command就是即将执行的命令。若Hook在Sub-Agent内触发,输入中还会有agent_id与agent_type。
exit code语义
只有command类型以exit code表达意图。
| exit code | 行为 |
|---|---|
| 0 | 成功。若stdout是JSON则解析为决策,否则继续正常流程 |
| 2 | 阻止。对可阻止事件生效,stderr内容反馈给Claude;对不可阻止事件仅显示stderr |
| 其他 | 视为脚本自身故障,流程继续。stderr首行连同Hook错误通知显示,完整输出记入调试日志 |
exit 2的设计把"有意阻止"与"脚本坏了"分开:脚本自身的故障,不应该拦下正常的工作流。
JSON输出
exit code是粗粒度的,细粒度控制用exit 0加stdout JSON。顶层字段如下。
| 字段 | 说明 |
|---|---|
continue |
设为false时Claude停止处理,配合stopReason给出原因 |
decision |
取值"block",用于Stop/PostToolUse/UserPromptSubmit,配合reason说明缘由 |
additionalContext |
注入Claude上下文的额外信息 |
systemMessage |
显示给用户的警示文本 |
suppressOutput |
隐藏stdout |
terminalSequence |
向终端发送转义序列,可用于通知、改标题 |
hookSpecificOutput |
事件特定的控制,见下 |
hookSpecificOutput里装的是随事件而异的字段:hookEventName声明事件名;PreToolUse用permissionDecision(allow/deny/ask/defer)与permissionDecisionReason表达权限决策,用updatedInput重写工具参数;PermissionRequest用decision: {"behavior": "allow"};PermissionDenied可返回retry: true;FileChanged/CwdChanged可返回watchPaths。
示例代码:
1 | { |
三条通用规则。
- exit 2与JSON输出不可混用
一旦以exit 2退出,stdout里的JSON会被忽略。 - 最严决策胜出
同一事件挂了多个Hook时,按deny>defer>ask>allow取最严格的决策。 additionalContext拼接
所有匹配Hook返回的additionalContext会拼接在一起交给Claude。
还有一条与权限系统的分界:PreToolUse返回allow可以跳过交互式确认,但不能越过权限规则里的deny。Hook可以比权限更严,不能比权限更松。
示例
我们用同一个场景把四个高频事件串起来:一个Node.js项目,团队约定是——破坏性命令必须拦截、写完文件立即格式化、测试没过不许收工、上下文压缩后要重申规范。四段配置都放在项目的.claude/settings.json,可以合并进同一个hooks键。
第一道,PreToolUse拦截危险命令。matcher限定Bash,脚本用jq取出即将执行的命令,发现rm -rf就以exit 2阻止,stderr里的原因会反馈给Claude。
示例代码:
1 | { |
第二道,PostToolUse自动格式化。matcher写Edit|Write,文件一写完就交给Prettier。Claude不需要知道、也不需要记得项目用什么格式化规范,这件事从"模型的义务"变成了"系统的性质"。
示例代码:
1 | { |
第三道,Stop测试闸门。Claude准备收工时跑一遍npm test,失败就以exit 2把它按回去继续修。
示例代码:
1 | { |
注意:Stop事件不支持matcher,所以条目里只有hooks列表。生产使用时,脚本还应检查输入里的stop_hook_active字段,防止测试一直修不好时陷入循环,详见本章经验。
第四道,SessionStart压缩后提醒。matcher取compact,只在上下文压缩后的新起点触发,把最容易在压缩中丢失的约定重新交代一遍。
示例代码:
1 | { |
四段配置合起来,这个项目就有了最小但完整的确定性防线:事前拦截、事后整形、收工把关、压缩兜底。
经验
- settings里的JSON不支持注释,尾逗号会报错
Hooks配置不生效时,先检查这两处。 - shell配置文件会污染输出
若~/.bashrc等文件里有无条件的echo(如欢迎语),会混进stdout导致JSON解析失败。用if [[ $- == *i* ]]把这类输出限制在交互式shell。 - matcher大小写敏感
bash匹配不到Bash工具,写错不报错、只是静默不触发。 if字段需要Claude Code v2.1.85+
更早的版本会无视它,过滤条件形同虚设。Stop连续阻止有8次上限
连续阻止8次后会被强制放行。脚本还应检查输入的stop_hook_active字段:为true说明当前这轮本就是被Stop Hook拦回来的,此时直接放行,避免死循环。PostToolUse不可撤销,只能补偿
它触发时工具已经执行,decision: "block"的语义是把问题反馈给Claude,不是回滚。真正要阻止的事,放到PreToolUse。- 多个Hook的执行顺序不确定
避免让多个PreToolUseHook修改同一工具的updatedInput。 - 路径占位符的引号
${CLAUDE_PROJECT_DIR}这类占位符在shell形式的command里要包双引号,exec形式(args)则不需要。 PermissionRequest只在交互模式存在
Headless模式(非交互模式)下没有权限对话,同样的控制改挂PreToolUse,见《7.Headless》。- Agent SDK同样支持Hooks
以回调形态声明,结构与CLI基本一致,差异见《8.SDK》。
最后把三层控制面的边界放在一起:CLAUDE.md是建议,模型大概率听;权限是静态策略,用allow/deny/ask规则事先划死边界,语法见《1.概述》;Hook是动态响应,在事件发生的瞬间用代码做决定。三者不互相替代,硬规则的正确做法是三层同时设防。
到这里,工具调用循环内部的每个节点我们都管得住了。但循环能做的事,上限始终是它的工具集,而内置工具止步于本地文件与shell——要让Claude Code接上数据库、Issue系统这些外部系统,需要一个标准协议,这就是《6.MCP》。