章节导航 第 11 章 / 共 23 章
Hooks 与生命周期
PreToolUse / PostToolUse / Notification 钩子系统、事件驱动的扩展模型、用户自定义脚本的安全执行机制
11.1 为什么需要 Hooks
工具系统(第 5 章)定义了 Claude Code 能做什么,权限系统(第 7 章)定义了它被允许做什么。但用户可能有更细粒度的需求:
- 在每次文件写入之前运行 lint 检查
- 在 Bash 命令执行之后记录审计日志
- 在特定工具调用时触发自定义通知
- 在 Claude Code 发送通知时执行额外操作
Hooks 系统提供了这种扩展能力——在工具执行的生命周期中插入用户自定义的脚本。
11.2 Hook 事件全景
官方 Hooks 参考文档的 “Hook lifecycle” 表当前列出 33 个事件(快照日期 2026-09-10)。此前本章写的是”28+ 种”——这个表述的问题在于无法对账:读者既不知道确切数字,也无法判断清单是否落后。下面按本书的分组方式逐一列出,并在最后给出对账。
需要注意:33 是官方逐字列出的事件数,不是”至少”或”约”。数量随版本变化,所以记录快照日期比记录数字本身更重要。这个数字也从最初的三种增长而来——Hooks 从”工具执行前后的两个插槽”长成了一套覆盖会话、权限、子代理、上下文、模型、显示的完整事件总线。按类别分组如下:
工具生命周期类
| Hook 事件 | 执行时机 | 典型用途 |
|---|---|---|
| PreToolUse | 工具执行前 | 检查、拦截、参数修改 |
| PostToolUse | 工具执行后 | 审计日志、后处理 |
| PostToolUseFailure | 工具执行失败后 | 错误通知、重试策略 |
| PostToolBatch | 一批工具调用全部结束后、下一次请求模型之前 | 注入依赖”这一批跑了哪些工具”的上下文 |
会话生命周期类
| Hook 事件 | 执行时机 | 典型用途 |
|---|---|---|
| SessionStart | 会话启动时 | 环境初始化、上下文注入 |
| SessionEnd | 会话结束时 | 清理、归档 |
| Setup | 初始化配置时 | 项目级配置检查 |
模型切换类(v2.1.251+)
这是 Hooks 系统中第一个监视模型本身的类别。此前所有事件监视的都是”模型做了什么”(PreToolUse 看 Bash 命令、PermissionRequest 看请求、PreCompact 看 transcript),而 PreModelSwitch / PostModelSwitch 监视的是”用哪个权重在回答”。
| Hook 事件 | 执行时机 | 可阻断 | 典型用途 |
|---|---|---|---|
| PreModelSwitch | 应用模型切换之前 | ✅ 可阻断 | 成本闸门(阻止升档)、企业授权范围(只允许特定模型)、敏感仓库阻止降级 |
| PostModelSwitch | 会话模型变化之后 | ❌ 不可阻断 | 审计”哪个模型写了这段代码”、成本追踪、按模型下发不同指引 |
触发范围需要精确界定——这一点上二手资料普遍写错,务必以官方为准。PreModelSwitch 只在你或客户端主动请求的切换时触发:
/model <name>与/model选择器Option+P/Alt+P模型选择器/config里的 Model 设置- 开启 fast mode 且该操作会改变会话模型时
- Agent SDK 宿主的
set_model请求,或apply_flag_settings请求中的模型变更
⚠️ 官方明确排除的两类切换:自动模型回退(automatic model fallback)和恢复会话时还原模型,Claude Code 不会运行
PreModelSwitch——这些是它自己造成的切换,只会到达PostModelSwitch。官方原话为 “Claude Code doesn’t run PreModelSwitch hooks for switches it makes on its own… Those changes reach PostModelSwitch only.”这个区分有实际后果:如果你的成本闸门只挂在
PreModelSwitch上,那么过载自动回退到更贵/更便宜的模型这条路是完全不受控的。要覆盖它必须改用PostModelSwitch做审计与事后纠正。还有一个更细的边界:fallback model chain 里的模型只服务一个 turn、不改变会话模型,因此PostModelSwitch也不会触发——单个 turn 的模型替换目前没有任何 hook 可见。
stdin 字段与 matcher 的两个特殊规则:
- 切换类事件收到的是
from_model/to_model,而不是单个model字段——官方明确只有SessionStart才会收到model。原因很直白:一次迁移无法用单个 model 字段描述。另注意没有$CLAUDE_MODEL环境变量;$ANTHROPIC_MODEL可以读但不会随切换改变,不能用它判断当前模型。 - matcher 匹配的是
to_model推导出的规范名(canonical name),忽略[1m]后缀。别名(如opus)、带日期的 ID、以及 Amazon Bedrock 这类供应商专属 ID,都会归到同一个规范名,所以claude-opus-5能覆盖 Opus 5 的所有写法。matcher 可写精确名、|分隔列表(claude-opus-4-6|claude-opus-5)或正则(.*opus.*)。 - 当 Claude Code 无法确定目标规范名时(例如只有你的 LLM gateway 认识的自定义模型 ID),它会无视 matcher、运行所有 PreModelSwitch hook。因此一个要阻断的 hook 必须自行检查输入里的
to_model,不能只依赖 matcher。
权限类
| Hook 事件 | 执行时机 | 典型用途 |
|---|---|---|
| PermissionDenied | 权限被拒绝时 | 审计告警 |
| PermissionRequest | 权限请求时 | 动态授权策略 |
Agent 协作类
| Hook 事件 | 执行时机 | 典型用途 |
|---|---|---|
| SubagentStart | 子代理启动时 | 资源分配监控 |
| SubagentStop | 子代理停止时 | 结果收集、状态同步 |
| TeammateIdle | 队友空闲时 | 任务再分配 |
| TaskCreated | 任务创建时 | 依赖检查 |
| TaskCompleted | 任务完成时 | 进度追踪 |
上下文管理类
| Hook 事件 | 执行时机 | 典型用途 |
|---|---|---|
| PreCompact | 上下文压缩前 | 保留关键信息 |
| PostCompact | 上下文压缩后 | 验证信息完整性 |
| CwdChanged | 工作目录变更时 | 项目上下文切换 |
| FileChanged | 文件变更时 | 自动格式化、测试触发 |
| ConfigChange | 配置变更时 | 策略热更新 |
| InstructionsLoaded | 指令加载时 | 指令验证 |
| WorktreeCreate | Worktree 创建时 | 环境初始化 |
| WorktreeRemove | Worktree 移除时 | 清理 |
| DirectoryAdded | 会话中通过 /add-dir 新增工作目录后 | 准备新仓库(如安装依赖) |
DirectoryAdded 有三个不触发的边界,值得记住:用 --add-dir 启动参数传入的目录不触发(由 SessionStart 覆盖);在 /permissions 的 Workspace 标签页添加的目录不触发;添加一个已经是或位于现有工作目录之内的目录不触发。它在沙箱与权限状态刷新之后才触发,所以 hook 里的沙箱工具已经能看到新目录。
用户交互类
| Hook 事件 | 执行时机 | 典型用途 |
|---|---|---|
| UserPromptSubmit | 用户提交提示时 | 输入预处理、过滤 |
| UserPromptExpansion | 用户键入的 slash command 展开为 prompt 之前 | 阻止特定命令被直接调用、为某个 skill 注入上下文、记录命令调用 |
| Notification | Claude Code 发送通知时 | 通知转发、自定义提醒 |
| Stop | 会话停止时 | 最终清理 |
| StopFailure | 停止失败时 | 错误恢复 |
| Elicitation | 请求用户输入时 | 日志 |
| ElicitationResult | 收到用户输入时 | 输入验证 |
UserPromptExpansion 的价值在于它补上了 PreToolUse 够不到的那条路径:一个匹配 Skill 工具的 PreToolUse hook 只在 Claude 调用该工具时触发,而用户直接键入 /skillname 会绕过 PreToolUse。UserPromptExpansion 正是在这条直达路径上触发的。它匹配的是 command_name——比如一个匹配 deploy 的 hook 可以在没有审批文件时阻止 /deploy。
Notification 此前只出现在 11.4 的配置示例和”最常用的 5 种 Hook”里,缺席全部分类表——这是本章清单长期落后的一项,现已补入。
显示控制类
| Hook 事件 | 执行时机 | 典型用途 |
|---|---|---|
| MessageDisplay | assistant 文本输出到显示层之前 | PII 脱敏、引用注入、审计追踪 |
MessageDisplay(v2.1.152,2026-05-27)是特殊的显示层 hook——它在模型输出文本渲染到终端/IDE 面板前触发,通过返回 displayContent 替换显示文本,但 transcript 和模型上下文保留原始内容。不支持 matcher,每次 assistant message 都触发。
性能警告:每次 assistant turn 都触发,延迟敏感——保持 <50ms,用 sed/awk 而非网络调用。
合并风险:多个 hook 返回
displayContent时合并顺序未定义——建议只注册一个 rewriting hook。
典型场景:
- 屏幕共享时 PII 脱敏(实时替换敏感信息显示)
- 引用注入(在显示层自动添加文档引用)
- Banner 注入(在输出前添加合规声明)
- 审计追踪(在显示内容中嵌入溯源标记)
清单对账(对照官方 33 个事件):工具生命周期 4 + 会话生命周期 3 + 模型切换 2 + 权限 2 + Agent 协作 5 + 上下文管理 9 + 用户交互 7 + 显示控制 1 = 33。与官方清单逐项一致,无缺项。下次版本更新后重做这道加法,是判断本章是否落后的最快方法——把模糊的”28+“换成可对账的数字,本身就是一种维护机制。
换一个轴:按运行时形态切分。 上面八组是按功能分的,回答”这个事件管什么事”;官方生命周期图暗含了另一种切法,回答的是“这个事件能不能挡住主流程”——写 hook 时后者往往更先用到:
| 形态 | 事件 | 意味着什么 |
|---|---|---|
| per-turn loop 内 | SessionStart UserPromptSubmit UserPromptExpansion PreToolUse PermissionRequest PostToolUse PostToolUseFailure PostToolBatch SubagentStart SubagentStop TaskCreated TaskCompleted Stop / StopFailure TeammateIdle PreCompact PostCompact SessionEnd | 处在主循环的路径上,其返回会直接影响这一轮怎么走 |
| 嵌套在 MCP 工具执行内 | Elicitation ElicitationResult | 只在 MCP 服务器请求用户输入时出现,是 33 个事件里唯一一对”工具内部”的事件(详见 11.4.2) |
| standalone async | WorktreeCreate WorktreeRemove Notification ConfigChange InstructionsLoaded CwdChanged FileChanged DirectoryAdded | 异步旁路,适合做观测与通知,不应用来阻塞 |
| standalone sequential | PreModelSwitch | 在模型切换前同步执行,也是唯一一个超时 fail-closed 的事件(见 11.5) |
| display-only | MessageDisplay | 只影响显示文本,transcript 与模型上下文保留原内容 |
⚠️ 注意 PermissionDenied 不在 PermissionRequest 的响应路径上,而是 auto 模式拒绝时的独立分支——它是 33 个事件里唯一一个”由另一个事件的特定结果触发”的事件。
最常用的 5 种 Hook 是 PreToolUse、PostToolUse、Notification、UserPromptSubmit 和 SessionStart——它们覆盖了绝大多数扩展需求。
企业级 Inference Hooks(v2026-08-05)
2026 年 8 月 5 日,Anthropic 发布了 Claude Enterprise 专用的 Inference Hooks——一种服务器端钩子机制,与上述客户端 Hooks 形成互补:
| 维度 | 客户端 Hooks(本章主体) | Inference Hooks |
|---|---|---|
| 部署位置 | 客户端 CLI / VS Code 扩展 | Anthropic 服务器端 / API Gateway 层 |
| 触发时机 | 工具调用前后、会话生命周期 | Prompt 到达模型前(PreModelInference)、模型响应后(PostModelInference,roadmap) |
| 控制粒度 | 工具级 + 事件级 | 请求级(二进制判定:allow/deny) |
| 适用场景 | 开发工作流自动化 | DLP 数据防泄露、合规审批、审计日志注入 |
| 覆盖范围 | Claude Code 客户端 | claude.ai + Cowork + Claude Code(组织级统一配置) |
| 配置权限 | .claude/settings.json 本地文件 | 组织 organization:manage 权限(Admin/Owner/Primary owner) |
核心工作流:企业配置一个 HTTPS 安全服务器端点 → Claude 在每次推理前将对话全文(PromptFrame)签名发送到该端点 → 服务器返回 allow/deny → deny 时请求不抵达模型。此功能目前处于 beta 阶段,仅面向 Claude Enterprise 客户,已集成 Palo Alto Networks、Netskope、Proofpoint、Zscaler 等 DLP 厂商。
架构意义:Inference Hooks 将安全边界从”客户端本地”推进到”模型推理之前”——以前 Claude Code 的 Hooks 只能拦截用户的 shell 命令和文件操作,Inference Hooks 可以拦截 prompt 本身。两者是互补非替代关系。
Hook 配置在 .claude/settings.json 中:
{
"hooks": {
"PreToolUse": [
{
"matcher": "FileWrite",
"command": "eslint --fix ${file}"
}
],
"PostToolUse": [
{
"matcher": "Bash",
"command": "logger 'Bash executed: ${command}'"
}
],
"Notification": [
{
"matcher": "",
"command": "notify-send '${title}' '${body}'"
}
]
}
}
matcher 支持工具名称匹配——空字符串匹配所有工具/通知,具体名称匹配特定工具。
11.3 Hook 的执行流程
11.3.1 五种 Hook 类型
Hook 类型从最初的单一 command 扩展为 5 种,每种类型有不同的执行模型:
| 类型 | 执行方式 | 输出格式 | 适用场景 |
|---|---|---|---|
| command | 执行 shell 命令 | stdout 作为返回 | 外部工具集成、脚本自动化 |
| http | POST 到 webhook 端点 | HTTP 响应体 | 远程服务通知、企业集成 |
| mcp_tool | 直接调用 MCP 工具 | 工具返回值 | 无需 shell 的结构化操作 |
| prompt | 用 Claude 自身评估 | 判断结果 | 上下文感知的条件判断 |
| agent | 生成子代理多步验证 | 验证报告 | 复杂条件的多步推理检查 |
特别值得注意的是 prompt 和 agent 两种类型——它们代表了”用 AI 控制 AI”的新范式。prompt 类型让 Claude 自己判断某个条件是否满足(例如”这段代码是否符合安全规范”),agent 类型则更进一步,生成一个子代理执行多步验证(例如”检查这个 PR 的所有文件是否符合项目规范”)。这与 Hooks 作为”确定性执行层”的定位形成了有趣的张力:Hooks 的一半是确定性的脚本执行,另一半是 AI 的弹性判断。
Hook 在工具执行流程中的位置(回顾第 5 章的工具执行生命周期):
1. Schema 验证 → Zod.safeParse(input)
2. 业务规则验证 → tool.validateInput(input)
3. ★ PreToolUse Hook → 用户自定义前置钩子 ★
4. 权限检查 → checkPermissionsAndCallTool()
5. 工具执行 → tool.call(input)
6. 结果格式化 → mapToolResultToToolResultBlockParam()
7. ★ PostToolUse Hook → 用户自定义后置钩子 ★
8. 结果返回 → yield 给 QueryEngine
注意 PreToolUse Hook 在权限检查之前执行——这意味着 Hook 可以在权限系统之外增加额外的控制层。
11.4 Hook 的权限决策
PreToolUse Hook 可以返回权限决策,直接影响工具是否被执行:
| Hook 返回 | 效果 | 使用场景 |
|---|---|---|
allow | 跳过后续权限检查,直接允许 | 企业白名单系统 |
deny | 直接拒绝,不执行工具 | 外部审计系统一票否决 |
| 无返回 / 其他 | 继续正常的权限检查流程 | 仅做日志记录 |
退出码协议。command 类型 Hook 的退出码与权限决策有明确映射:
| 退出码 | 含义 | 权限效果 |
|---|---|---|
| 0 | 成功继续 | 等同于 allow |
| 2 | 阻塞 | 等同于 deny(仅 PreToolUse) |
| 其他非零 | 错误但不阻塞 | 继续正常权限检查 |
匹配器增强。matcher 支持正则表达式(如 "Bash(git *)" 匹配所有 git 开头的 Bash 命令)、if 条件过滤、once: true 仅执行一次——这些增强使 Hook 配置更加灵活和精确。
matcher 有两个不写在文档标题里的坑,都已在 2026 年 9 月被修掉。
第一个是被匹配字段缺失时的行为,与”空 matcher 匹配一切”是两回事:SubagentStop 上带特定 matcher 的 hook,此前会对每一个 agent type 为空的停止中的子代理触发(v2.1.275 修复)。也就是说,matcher 写得很具体并不代表触发面就窄——当被匹配的那一栏是空值时,“具体”会被解释成”匹配”。写 matcher 时要想清楚目标字段可能为空的场合。
第二个是 matcher 名与 JavaScript 原型链的相撞:hook matcher 若以继承来的对象属性命名(如 __proto__ 或 constructor),/hooks 交互菜单会直接崩溃(v2.1.271 修复)。这条对一本”源码解剖”的书尤其有信息量——它说明 matcher 名在实现上被当成了对象键使用,__proto__ 因此不是普通字符串。同源的问题在同一时期还出现在别处:v2.1.277 修了 /plugin → Installed 与 /skills 在 skill 或 legacy command 取名为 constructor、toString 这类内建属性时崩溃。一个把用户输入当键用的实现,在所有这类入口上都要防同一件事。
这意味着 Hook 可以作为一个外部权限源——比如连接到企业的安全审计系统,在 Claude Code 内置权限系统之外增加一层控制。这对于需要合规审计的企业场景特别有价值。
输出写错时会怎样。上面讨论的都是”hook 正确返回时如何解释”,还有一个同样重要的问题:hook 输出格式非法时会发生什么。11.5 节会讲到超时、进程隔离、输出限制、配置源四类边界,但在此之前先补上更常见的一类——输出本身不合格。
| 情形 | 旧行为 | 现行为(v2.1.248 起) |
|---|---|---|
hook 往 stdout 打印一个不是合法 JSON 的 {…} 对象 | 被静默当作纯文本处理 | 报为 hook 错误并附带解析信息 |
后台会话中 PermissionRequest / PreToolUse hook 打印无效答案 | 静默等待 | claude agents 行指明是哪个 hook 及其 schema 错误 |
第二条值得单独强调。hook 输出错误在后台会话中的表现是挂起,不是报错。一个跑在后台的 agent 卡住不动、日志里没有任何异常,最可能的原因不是网络或模型,而是某个 hook 吐出了一句格式不对的 JSON。这类故障之所以难查,是因为它在现象上与”模型在思考”完全一样。诊断它需要与 11.5 节的超时机制配合使用:先确认不是 hook 挂起,再排查其他方向。
配套的还有 v2.1.246 的一条小修复:hook 错误信息里的 ${CLAUDE_PLUGIN_ROOT} 曾以字面量显示,现在解析为真实插件路径。这看起来只是显示问题,但当错误信息是你唯一的线索时,一个没被展开的变量会让整条信息失去指向性。
11.4.1 输出改写:第四种权限模式
除了 allow/deny/无返回三种权限决策模式,PostToolUse Hook 在 v2.1.121 起引入了输出改写模式——不改变执行权限,但改写模型视角的工具输出:
| 模式 | 机制 | 效果 |
|---|---|---|
| 允许 | allow | 跳过权限检查,直接执行 |
| 拒绝 | deny | 阻止执行 |
| 默认 | 无返回 | 继续正常权限检查 |
| 改写 | updatedToolOutput | 执行后替换模型看到的输出 |
updatedToolOutput 字段允许 Hook 将工具真实输出替换后再呈现给模型——适用于所有工具类型,不仅仅是 Bash。Transcript 中保留原始输出,只有模型上下文中被改写。
典型场景:
- 密钥脱敏:
kubectl get secrets输出中扫描并脱敏 AWS/Azure 密钥后,再将脱敏版呈现给模型 - 输出压缩:50KB 的测试输出压缩为一段摘要,减少上下文占用
- 跨平台规范化:将 macOS 风格路径转为 Linux 风格,使模型在跨平台协作中不受路径格式干扰
与 additionalContext 的区别:updatedToolOutput 是替换模型看到的工具输出,而 additionalContext 是追加额外信息——前者控制模型看到什么,后者补充模型还应该知道什么。
风险警告:(1) 脱敏后模型无法看到真实值,可能影响后续推理——需确保脱敏的字段后续不需要;(2) 多 hook 同时返回
updatedToolOutput存在竞态,建议每个 matcher 只注册一个 rewriting hook;(3) 静默错误屏蔽——如果 hook 过滤了 stderr 中的警告信息,模型可能做出错误决策。
11.4.2 Elicitation:退出码 2 通则的唯一例外
上面的退出码协议有一个必须先记住的边界。官方 hooks 文档对退出码 2 的通则是:“Exit 2 means a blocking error… Claude Code still reads any valid JSON output on stdout.” 也就是说,即便退出码已经是 2,stdout 上的 JSON 仍会被读(11.4.1 的 updatedToolOutput 与 decision 都依赖这条)。
但 Elicitation 与 ElicitationResult 是这 33 个事件里唯一的例外:“On Elicitation and ElicitationResult, an exit-2 hook’s hookSpecificOutput is ignored.” 读者若按通则给这两个事件写退出码 2 的 hook,会以为 content 里的表单字段还能被读到——读不到。
两个事件的语义与 block 行为:
| 事件 | 触发时机 | matcher | 支持 block | block 的效果 |
|---|---|---|---|---|
Elicitation | MCP 服务器在工具调用中请求用户输入时 | MCP 服务器名 | 是 | 拒绝该次 elicitation |
ElicitationResult | 用户响应后、回传服务器之前 | 同上 | 是 | 阻断响应(action 变为 decline) |
hookSpecificOutput 支持 action(accept / decline / cancel)与 content(前者为表单字段值,后者可改写)。⚠️ {"decision":"block"} 在这两个事件上此前是被静默忽略的——v2.1.284 才修好,现在它的效果等同退出码 2。也就是说,在 v2.1.284 之前,一个写了 block 的 Elicitation hook 什么也不会发生。配套的 Notification 事件同期新增了四个 type:elicitation_dialog、elicitation_url_dialog、elicitation_complete、elicitation_response。协议侧的对应物是第 10 章 10.2.1 的 MRTR(SEP-2322)——MRTR 让 elicitation 在无状态服务器上成为可能,这两个 hook 让它在客户端侧可被拦截。
11.5 超时与安全
Hook 脚本有严格的安全边界:
| 约束 | 机制 | 目的 |
|---|---|---|
| 超时限制 | 可配置的执行超时 | 防止失控脚本阻塞 Claude Code |
| 进程隔离 | 子进程执行 | 不能直接修改 Claude Code 内部状态 |
| 输出限制 | 返回值格式约束 | 只能通过约定的返回格式影响行为 |
| 配置源约束 | 只从 settings.json 读取 | 不能在运行时动态注入 Hook |
| 前缀稳定性 | hook 输出进入提示前缀,跨轮次必须稳定 | 防止 hook 输出变化导致整段 prompt cache 失效 |
v2.1.139 安全增强:
- args[] 直接派生:Hook 新增
args: string[]字段(exec form),直接 spawn 命令无需通过 shell,路径占位符不再需要转义——消除了 shell 注入风险 - continueOnBlock 配置:PostToolUse Hook 新增
continueOnBlock: true选项,当 Hook 拒绝操作时仍将拒绝原因反馈给 Claude 并继续执行任务——允许”软拒绝”而非”硬阻塞”
v2.1.119 duration_ms 字段。PostToolUse Hook 的 stdin JSON 新增了 duration_ms 字段——工具调用的实际耗时(毫秒)。这对于构建性能监控 Hook 是关键数据点:
{
"tool_name": "Bash",
"tool_input": {...},
"tool_output": "...",
"duration_ms": 1234
}
结合 PostToolUse Hook,可以在不侵入 Claude Code 内部的情况下实现工具调用延迟基线、慢查询检测和 SLA 告警——典型的带外监控(out-of-band monitoring)模式。
SessionStart 的恢复成本字段。v2.1.251 起,当 source 为 "resume" 或 "fork" 且 transcript 已有回复时,SessionStart hook 会额外收到四个字段:
| 字段 | 含义 |
|---|---|
seconds_since_last_response | 距 transcript 中最后一条回复的墙钟秒数 |
context_tokens | 恢复会话的首个请求将重新发送的 prompt token 数 |
prompt_cache_likely_expired | 缓存是否可能已过期(最后一条回复超过 prompt cache 存活期,或后续压缩替换了缓存对话时为 true) |
estimated_cache_write_usd | 把 context_tokens 写入 prompt 缓存的预估美元成本(按会话当前模型计,不含响应) |
官方示例是一个在上次回复 90 分钟后被恢复的会话:seconds_since_last_response 为 5400、context_tokens 为 182340、prompt_cache_likely_expired 为 true、estimated_cache_write_usd 为 1.1396。
这组字段把 SessionStart 的能力从”注入上下文”扩展到了”在恢复发生之前判断这次恢复贵不贵”。hook 可以据此决定是否提示用户、是否先跑一次 warmup、甚至在预估成本超过阈值时要求确认。这与第 9 章的 Prompt Caching 讨论是同一件事的两面:第 9 章讲的是缓存如何影响成本,这里讲的是在成本发生前你能拿到哪些数字。官方对该改动的说明也正是”让一个判断是否要提醒你缓存问题的 hook 有真实数字可用”。
Skill 内 Hook 作用域。Skill 可以定义自己的 Hook 作用域,这些 Hook 只在 Skill 激活时生效。这为 Skill 提供了”自带安全防护栏”的能力——例如,数据库操作 Skill 可以自带”禁止 DELETE 无 WHERE 子句”的 PreToolUse Hook,且只在 Skill 激活时生效。
Skill 级工具治理。除了 Hook 作用域之外,v2.1.152 将 Skill 的管控能力扩展到工具可用性层面——从 “执行时干预” 升级为 “工具可用性治理”:
- disallowed-tools frontmatter:Skill 可在 frontmatter 中声明
disallowed-tools,在 Skill 激活期间直接从模型的工具面板中移除指定工具——比 PreToolUse deny 更彻底,模型甚至不知道自己 “失去了” 某个工具 - /reload-skills 命令:无需重启 Claude Code 即可重载 Skill 配置,降低开发迭代成本
- SessionStart reloadSkills:SessionStart Hook 可返回
reloadSkills: true,在会话启动时自动刷新 Skill 配置——实现 Skill 配置热更新
这代表了 Hook 系统从 “执行时干预” 扩展到 “工具可用性治理” 的新维度:前者在工具已选中的前提下进行拦截,后者在工具选择阶段就从模型中移除工具。适用于需要严格管控的受监管环境——例如金融场景中禁止 AI 执行任何网络请求工具的 Skill。
第五类边界:前缀稳定性。新增的这一行与前面四类性质不同——前四类约束的是”hook 能做什么”,这一类约束的是”hook 输出的内容与顺序会在成本层面造成什么后果”。hook 输出不是只在当下生效的一次性副作用,它是提示前缀的一部分:它在轮次之间必须保持稳定,否则整段 prompt cache 失效。这与第 9 章”前缀稳定性 > 单次压缩量”是同一原理,只是此前本章完全没有讨论 hook 对缓存的影响。
四条官方修复把这条边界的后果具体化了:
| 版本 | 问题 | 说明 |
|---|---|---|
| v2.1.265 | agent teammates 与已恢复子 Agent 在后续轮次把 SubagentStart hook 上下文与预加载 skill 移出提示前缀 | 破坏了 prompt cache 复用 |
| v2.1.261 | 恢复会话时丢失 hook 输出及并行工具调用周围的上下文 | 恢复后的请求与原始请求不一致——不只是慢,而是变了 |
| v2.1.259 | 阻塞型 Stop hook 让其后的那一轮丢失模型的推理内容,部分模型上还会 miss prompt cache | 阻塞的代价不止是等待 |
| v2.1.247 | 打印数 MB 错误输出的 hook 可能撑爆会话并卡在 “Prompt is too long” | 给”输出限制”这一约束补上了具体失效后果 |
| v2.1.277 | /clear 后续接的会话(restart、--continue、--resume)在 SessionStart hook 打印了输出时丢失首条消息的一部分,导致完整的 prompt cache miss | 最强的一例:hook 只要”有输出”就足以打掉整段缓存,与输出内容是否变化无关 |
| v2.1.274 | Stop prompt hook 每次被 block 都重发整个 prompt;现改为只发带 500 字符标签的条件名 | prompt 型 hook 的注入量随 block 次数线性膨胀 |
第三条尤其值得注意:一个阻塞型 Stop hook 的代价不只是”用户多等一会儿”,它会让下一轮丢失模型上一轮的推理内容。这条把 11.4 节”软拒绝”(continueOnBlock)的价值从体验问题提升为正确性问题——能软拒绝就别硬阻塞。
v2.1.277 这一例把边界的口径推到了最严处。 前四条都还是”hook 输出变了所以缓存失效”,读者会自然理解为”只要输出稳定就没事”。v2.1.277 说的是另一件事:/clear 之后继续的会话里,只要 SessionStart hook 打印了任何输出,首条消息的一部分就会丢失,进而造成整段 prompt cache miss。风险点从”输出是否稳定”前移到了”是否有输出”——一个只 echo 一句状态、内容永远不变的 hook,同样会触发。
v2.1.274 那条则揭示了 prompt 型 hook 特有的一类膨胀:每次被 block 都把整个 prompt 重新注入一遍,注入量随 block 次数线性增长,而这类 hook 恰恰是被设计成”不满足条件就一直拦”的——拦得越久,前缀越长。改成 500 字符的条件标签之后,重复块的体积才与 block 次数脱钩。同源的还有 v2.1.277 一条:恢复的 subagent 与 teammate 重新渲染它们已加载的 MCP 工具定义,同样会打掉那个 agent 的 prompt caching。
可观测性:失败与超限都有了新的落点。 上面讨论的失效大多要靠现象反推,2026 年 9 月下旬补上了三类直接信号:
- v2.1.284:失败 hook 的 stderr 在 hook 同时写了 stdout 时会被 debug log 丢弃,且一个没有任何输出的失败 hook 此前什么都不记录——现在失败 hook 也会记录状态码。这条对 11.4.1 提到的”后台会话挂起”尤其关键:此前那种”日志里什么都没有”的故障,有一半是日志本身漏了。
- v2.1.280:
hook_execution_completeOpenTelemetry 事件新增输出体积与超限输出被存入文件的数量。这补上了 11.4 那条边界的另一半——超限输出不是被丢弃,而是改道落盘,且落盘数量现在是可观测的。 - v2.1.271:SessionStart / UserPromptSubmit / PreToolUse / SessionEnd hook 运行时,spinner 会说明正在跑哪个 hook 并计时,Esc 可取消等待 SessionStart hook 的提示。执行环境的可用性同期也有两条:v2.1.281 起
claude --bg在未通过 workspace trust 提示的目录不再直接跑项目 hook(改为先要 trust,非交互则退出);mcp_tool型 hook 在阻塞事件上不再因 MCP 服务器仍在连接而被跳过,改为等待至 MCP 连接超时。另有 v2.1.280 修的后台会话在环境变量含 NUL 字符时无法运行 git / hook / 插件,以及 v2.1.268 前PermissionRequesthook 在--print模式下根本不触发。
结论是:hook 不只是安全边界,也是缓存边界。设计 hook 时,输出什么、按什么顺序输出、以及输出是否跨轮次稳定,都应作为提示前缀的一部分来考量。
超时是 Hook 安全的关键——如果一个 PreToolUse Hook 因为网络请求卡住了 30 秒,整个 Claude Code 不应该被阻塞。超时后 Hook 被强制终止,工具执行按”无 Hook 返回”处理(继续正常权限检查)。
⚠️ 但这个”超时即放行”的结论有一个例外,且是官方明示的:
PreModelSwitch是 fail-closed 的。官方 hooks 文档的原话是 “OnPreModelSwitch, a hook canceled at its timeout blocks the model switch”——换言之,在绝大多数事件上,超时意味着”hook 没来得及给决策,于是当作没有决策”;而在PreModelSwitch上,超时意味着切换被阻断。同文档的决策表也把该事件标记为 “Yes | Blocks the model switch and shows stderr to the user”。
超时不是一个数,而是一张矩阵。 上面把超时讲成了单一默认值,官方实际给的是按类型与按事件两级下调:
| 层级 | 默认超时 |
|---|---|
| 按 hook 类型 | command / http / mcp_tool 600 秒;prompt 30 秒;agent 60 秒 |
按事件下调(command/http/mcp_tool) | UserPromptSubmit / PreModelSwitch / PostModelSwitch 降到 30 秒;MessageDisplay 降到 10 秒 |
SessionEnd | 共享 1.5 秒预算(不是每个 hook 各 1.5 秒) |
SessionEnd 那一行是整张表里最反直觉的:它比任何一类默认值都小一到两个数量级,而且是多个 hook 合起来只有 1.5 秒。这直接解释了一类常见困惑——“我的 SessionEnd hook 为什么总是不执行完”。它不是没跑,是被 1.5 秒的共享预算掐断了。抬预算有两条路:给单个 hook 设 timeout(整体预算会自动抬到与之匹配,上限 60 秒,但插件提供的 hook 上的 timeout 不会抬预算),或设 CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS。⚠️ 后一条在 v2.1.268 之前只能抬整体预算,没有自己 timeout 的 hook 仍会在 1.5 秒后被取消——设了环境变量却没生效,正是这个版本差。
失效方向也会翻转:托管 hook 白名单从 fail-open 改为 fail-closed。 上面讨论的是单个 hook 的超时,治理侧另有一条方向性修正更值得记住:v2.1.267 起,托管设置 allowedHttpHookUrls、httpHookAllowedEnvVars、allowedChannelPlugins 在不可读时,从此前的”放行一切”改为”不放行任何东西”。这是白名单失效方向的一次翻转——读不到配置时,旧行为等于把整套 hook 管控静默关掉。对安全论证是正面材料,但运维上的后果是:配置文件损坏会直接让所有 HTTP hook 停摆,而不是像过去那样”照常工作”。
实践含义很直接:写一个会做网络调用的
PreModelSwitchhook 是危险的。网络抖动不会让切换”照常进行”,而会让切换失败。这与本章其余部分的直觉完全相反——读者若照搬”超时放行”的通用理解去写这个 hook,会得到”模型永远切不了”而不是”切换照常”。建议给足timeout,并显式设计超时兜底路径(例如超时时默认输出 allow),而不是依赖系统的默认放行。这也是为什么
PreModelSwitch在官方生命周期图里被标为 standalone sequential event——它是顺序执行的,Claude Code 会等它返回再继续。相对地,PostModelSwitch是 async 事件,且因为模型已经变了,它无法阻断任何事。
源码引用:Hook 系统实现位于 src/utils/hooks.ts,包含 Hook 执行、匹配逻辑和超时控制。
11.6 实际应用场景
几个真实的 Hook 使用场景:
1. 代码格式化前置检查:
{
"matcher": "FileWrite",
"command": "prettier --check ${file} || echo 'deny'"
}
在文件写入前运行 Prettier 检查,格式不合规则拒绝写入。
2. Git 操作审计:
{
"matcher": "Bash",
"command": "echo \"$(date) Bash: ${command}\" >> ~/.claude-audit.log"
}
记录所有 Bash 命令到审计日志。
3. Slack 通知集成:
{
"matcher": "",
"command": "curl -X POST $SLACK_WEBHOOK -d '{\"text\": \"${title}: ${body}\"}'"
}
把 Claude Code 的通知转发到 Slack 频道。
11.7 设计启示
Hooks 是 Claude Code 扩展性的关键支柱。它的设计遵循了一个原则:开放扩展点,而不是开放内部实现。用户不需要修改 Claude Code 的代码,只需要在约定的时机点插入自己的脚本。
这种设计在企业场景中尤其重要——IT 部门可以通过 Hooks 实施安全策略和审计要求,而不需要 fork Claude Code 的代码库。