【笔记】Claude Code Hook 的工具调用控制与消息注入机制
Claude Code Hook 的关键价值,不只是“在某个时机运行脚本”,而是让确定性程序进入概率性的 Agent 循环:宿主可以在生命周期节点读取实时状态、控制工具行为,并把反馈送进模型的后续推理。
这篇笔记重点回答三个问题:
- 一次事件如何经过
matcher和if找到真正要执行的 handler; - Hook 怎样影响工具执行与权限链;
additionalContext怎样进入下一次模型请求。
本文不枚举所有 Hook 事件,也不把 Claude Code 当前的内部消息排版当作稳定协议。公开字段和行为以官方文档为准;attachment、<system-reminder> 和消息合并过程只用于理解当前实现。
1. 先把 Hook 放回完整的上下文装配模型
Claude Code 中,模型看到的内容不只来自用户输入。宿主会在不同生命周期,以不同方式装配额外上下文:
| 来源 | 触发方式 | 主要作用 | 是否属于硬控制 |
|---|---|---|---|
| CLAUDE.md、rules、memory | 会话启动或路径访问 | 提供相对稳定的项目指令 | 否 |
| 文件变更 attachment | 已读文件被外部修改 | 告诉模型旧快照已经变化 | 否 |
| Skill | 用户或模型显式选择 | 注入一段可参数化的工作流指令 | 否 |
| Hook | 生命周期事件自动触发 | 控制流程、改写数据或追加实时上下文 | 取决于输出通道 |
前三类解决“模型应该知道什么”。Hook 还可以解决“宿主是否允许这件事发生”。这正是 Hook 与普通上下文加载机制的核心差别。
flowchart LR
A[稳定指令<br/>CLAUDE.md] --> M[消息装配]
B[按需指令<br/>Skill] --> M
C[环境变化<br/>attachment] --> M
D[实时反馈<br/>Hook additionalContext] --> M
M --> L[下一次模型调用]
H[Hook 控制与数据输出] --> R[本次宿主行为]
R --> T[工具执行或拒绝]
T --> M
因此,additionalContext 应理解为 Hook 的一条输出通道,而不是 Hook 的全部能力;Hook 也不应被归类成另一种 CLAUDE.md。
2. Hook 是 Agent 循环中的确定性控制点
Claude Code 可以粗略拆成模型和宿主两部分:
1
2
3
4
5
6
7
Claude 模型:推理并生成 tool_use
↓
Claude Code 宿主:检查权限、执行工具、维护消息
↓
工具结果:包装为 tool_result
↓
Claude 模型:读取反馈并继续推理
模型擅长开放式判断,但具有概率性,也不天然知道剩余时间、CI 状态、审批结果等实时信息。宿主掌握工具、权限和消息组装,可以提供确定性保证。
Hook 就是宿主暴露的生命周期扩展点:
1
2
3
4
5
6
7
8
9
10
11
12
13
生命周期事件
↓
matcher 等条件筛选
↓
执行 Hook handler
↓
解析退出码与结构化输出
├─ 控制宿主流程
├─ 修改工具输入或模型可见结果
├─ 向后续模型调用追加上下文
└─ 记录日志、发送通知等外部动作
↓
继续或停止 Agent 循环
一句话记忆:
Hook 让外部确定性程序在 Agent 生命周期的指定位置观察、干预并反馈。
2.1 为什么 Prompt 不能替代 Hook
Prompt 可以要求模型不要运行危险命令,但是否遵守仍取决于模型。PreToolUse 的拒绝则由宿主执行,可以在副作用发生前阻止调用。
Prompt 也无法自动获得不断变化的外部状态。Hook 可以在事件发生时查询这些状态,再把结果变成:
- 权限决定;
- 修改后的工具参数;
- 修改后的模型可见结果;
- 下一轮推理所需的补充上下文。
因此,稳定原则可以留在 Prompt;必须强制执行的约束、实时状态和行动后的反馈更适合放在 Hook 或其他宿主机制中。
2.2 与 AOP 的类比边界
| Claude Code | Java AOP | 作用 |
|---|---|---|
| Hook 事件 | Join point | 预先定义的生命周期节点 |
| matcher | Pointcut | 选择需要处理的事件或工具 |
| Hook handler | Advice | 真正执行的外部逻辑 |
| Hook 输出 | Advice 结果 | 控制流程、修改数据或追加上下文 |
这个类比只用于建立直觉。Hook 只能处理 Claude Code 明确定义的事件,不能拦截任意内部方法。
3. 先分清 run、turn、工具调用和 Observation
一次用户任务通常不是一次模型调用:
1
2
3
4
5
6
7
用户提交任务
│
├─ 模型调用 #1:生成 tool_use A、B
├─ 宿主执行 A、B,生成 tool_result A、B
├─ 模型调用 #2:读取结果,生成 tool_use C
├─ 宿主执行 C,生成 tool_result C
└─ 模型调用 #3:读取结果,输出最终答案
| 概念 | 本文含义 |
|---|---|
| Session | 一段可持续、可恢复的完整对话 |
| Agent run | 一次用户任务从提交到最终返回 |
| Agent turn | 模型调用工具、宿主执行并反馈结果的一次往返 |
| Tool call | 一次具体的 Read、Bash、Edit 或 MCP 调用 |
经典 ReAct 中的 Observation,在这里主要对应工具执行产生的数据;tool_result 是它的协议载体。下一次 API 调用不是 Observation 本身,而是负责把 Observation 送回模型。
1
2
3
4
5
6
7
8
9
Reason → Action(tool_use)
↓
宿主执行工具
↓
Observation 在本地产生
↓
包装成 tool_result
↓
下一次模型调用读取结果并继续 Reason
这个时间顺序决定了一个重要边界:PreToolUse 虽然发生在工具执行前,但模型已经生成了这次 tool_use。Pre Hook 返回的上下文不会让模型在工具执行前自动重推理一次。
4. Tool Use Hook 位于哪里
围绕一次工具调用,不能只看 Pre 和 Post。当前权限链还包括 PermissionRequest;auto mode 的拒绝另有 PermissionDenied,并行工具全部结束后还有批次级 PostToolBatch:
flowchart TD
A[模型生成 tool_use] --> B[PreToolUse]
B -->|deny| C[拒绝结果反馈给模型]
B -->|defer| X[保留调用并退出<br/>等待外部恢复]
B -->|ask / allow / 默认流程| P[权限规则与模式继续判断]
P -->|需要用户确认| D[PermissionRequest]
D -->|deny| C
D -->|allow| E[执行工具]
P -->|允许| E
P -->|auto mode 拒绝| Q[PermissionDenied]
Q -->|可选 retry true| C
E -->|成功| F[PostToolUse]
E -->|失败| G[PostToolUseFailure]
F --> J[等待同批工具全部结束]
G --> J
J --> K[PostToolBatch 恰好一次]
K --> H[下一次模型调用]
C --> H
PostToolUseFailure 只处理真正开始执行后发生的失败。输入校验失败、权限拒绝等情况有各自的路径,不能假设所有未成功调用都会触发它。
图中 PermissionDenied 不是所有拒绝事件的统一出口。当前官方契约下,它只在 auto mode 拒绝工具调用时触发;用户手动拒绝、PreToolUse 返回 deny 或配置中的 deny rule 命中,都不会触发它。
4.1 核心能力矩阵
| 能力 | PreToolUse | PostToolUse | PostToolUseFailure |
|---|---|---|---|
| 读取工具参数 | 可以 | 可以 | 可以 |
| 读取成功结果 | 不可以 | 可以 | 不可以 |
| 读取执行错误 | 不可以 | 不可以 | 可以 |
| 修改本次实际参数 | updatedInput | 不可以 | 不可以 |
| 阻止本次执行 | permissionDecision: deny | 工具已经执行 | 工具已经失败 |
| 替换模型看到的成功结果 | 不可以 | updatedToolOutput | 不适用 |
| 追加后续模型上下文 | additionalContext | additionalContext | additionalContext |
| 撤销现实副作用 | 工具尚未执行 | 不可以 | 不可以 |
这张表的重点不是背字段,而是区分三个时间点:执行前可以改变现实行为,执行后只能改变反馈,追加上下文则要到下一次模型调用才被读取。
4.2 三个周边事件各自补哪一段
| 事件 | 触发点 | 适合做什么 | 不能误解成什么 |
|---|---|---|---|
PermissionRequest | Claude Code 即将弹出权限确认,或无法交互时原本会自动拒绝 | 代表用户侧审批,允许、拒绝、修改输入或更新权限规则 | 不是每次工具调用都会触发 |
PermissionDenied | auto mode 已经拒绝工具调用 | 记录拒绝,或用 retry: true 告诉模型可以重试 | 不会撤销本次拒绝,也不覆盖所有拒绝来源 |
PostToolBatch | 同一批全部工具调用已经结束、下一次模型请求之前 | 基于整批结果汇总一次上下文,或阻止 Agent 继续循环 | 不是每个工具都会触发一次 |
PermissionRequest 与 PreToolUse 的位置不同。Pre Hook 在每次工具调用的权限判断前运行;PermissionRequest 只在确实需要权限决定时运行。它可以代替用户回答一次权限请求:
1
2
3
4
5
6
7
8
9
10
11
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow",
"updatedInput": {
"command": "npm run lint"
}
}
}
}
即使返回 allow,修改后的参数仍会重新经过 deny 和 ask 规则;Hook 不能用一次允许决定绕过更严格的权限规则。
PermissionDenied 的 retry 也只是改变后续认知:
1
2
3
4
5
6
{
"hookSpecificOutput": {
"hookEventName": "PermissionDenied",
"retry": true
}
}
它会告诉模型可以调整后重试,但不会让刚刚被拒绝的调用恢复执行。若 auto mode 没有得到可用的分类结论,Claude Code 还可能忽略 retry: true,继续保持安全拒绝。
PostToolBatch 则解决并行工具的汇总问题。PostToolUse 和 PostToolUseFailure 仍按单个工具触发;当整批调用全部 resolved 后,PostToolBatch 恰好触发一次,而且没有 matcher。它拿到整批 tool_calls,适合追加依赖整组结果才能形成的说明:
1
2
3
4
5
6
{
"hookSpecificOutput": {
"hookEventName": "PostToolBatch",
"additionalContext": "这批读取都属于账本模块,完成前统一运行 pytest。"
}
}
如果返回顶层 decision: "block" 或 continue: false,它会在下一次模型调用前停止 Agent 循环。
5. Hook 输出有三条语义通道
只按“Pre 和 Post”分类还不够。Hook 输出实际影响三个不同对象:
| 通道 | 影响对象 | 典型字段 |
|---|---|---|
| 控制通道 | 宿主是否继续或是否允许工具 | permissionDecision、continue |
| 数据通道 | 工具实际输入或模型可见结果 | updatedInput、updatedToolOutput |
| 认知通道 | 模型下一次推理所见信息 | additionalContext |
5.1 控制通道:决定本次动作是否发生
PreToolUse 可以在副作用发生前拒绝调用:
1
2
3
4
5
6
7
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "禁止在 main 分支强制推送"
}
}
它也可以返回 ask 强制进入用户确认,返回 allow 跳过普通交互式提示,或者在非交互调用中返回 defer,保留当前工具调用并等待外部宿主稍后恢复。
一次事件可能命中多条 Pre Hook。Claude Code 会先等待所有匹配的 Hook 运行完,再合并结果;其中一条返回 deny,不会阻止其它 sibling Hook 已经开始执行。因此,不能依赖“安全 Hook 先拒绝”来阻止另一条 Hook 自身产生副作用。
当前决策优先级是:
1
deny > defer > ask > allow
最严格的结果获胜,配置顺序不能让 allow 覆盖 deny。所有 Hook 返回的 additionalContext 会一起保留并交给模型。
多条 Hook 同时修改同一个 tool_input 则不安全:Hook 并行运行,顺序不确定,不应让最终结果依赖哪个 updatedInput 后合并。需要组合修改时,应收敛到同一个 handler 中按明确顺序处理。
Hook 自身的允许也不是权限系统的最高决定。deny 和 ask 规则仍会继续评估,所以 allow 可以减少普通提示,却不能绕过更严格的 deny rule。
顶层 continue: false 的含义更强:它停止 Claude Code 后续处理,而不只是拒绝某一次工具。stopReason 是给用户看的终止原因。
5.2 数据通道:改写实际输入或模型可见结果
Pre Hook 可以改写即将传给工具的参数:
1
2
3
4
5
6
7
8
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"updatedInput": {
"command": "npm test -- --runInBand"
}
}
}
Post Hook 可以用 updatedToolOutput 替换模型后续看到的成功结果。它适合脱敏、裁剪过大的输出,或把结果恢复成工具约定的结构。
但它不能撤销已经发生的文件写入、网络请求或其他副作用。替换模型可见结果,也不代表遥测和外部系统从未见过原始结果。
5.3 认知通道:给下一轮推理追加说明
additionalContext 保留原始工具结果,同时增加一段与当前事件有关的解释:
1
2
additionalContext:原始结果 + 补充说明
updatedToolOutput:原始结果 → 新的模型可见结果
一般应优先追加说明。只有确实需要脱敏、裁剪或修复输出结构时,才替换结果;不能借此静默伪造成功或隐藏影响判断的错误。
6. 为什么 Pre 的 additionalContext 不能改变本次决策
当 PreToolUse 开始时,本次模型调用已经结束,tool_use 已经生成。Hook 运行期间不会自动插入额外的模型调用:
1
2
3
4
5
6
7
模型调用 #1 已生成 tool_use
↓
PreToolUse 返回 additionalContext
↓
工具按权限决定执行或被拒绝
↓
模型调用 #2 才读取 additionalContext
因此:
| Pre 输出 | 影响对象 | 生效时间 |
|---|---|---|
updatedInput | 本次工具的实际参数 | 工具执行前 |
permissionDecision | 本次工具是否执行 | 工具执行前 |
additionalContext | 模型后续认知 | 下一次模型调用 |
如果目标是让模型放弃危险动作并重新选择方案,应该同时拒绝当前调用并给出替代方向:
1
2
3
4
5
6
7
8
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "当前命令可能破坏生产数据",
"additionalContext": "请改用只读查询核验"
}
}
只返回“这个命令可能很危险”的 additionalContext,并不会自动阻止本次工具。
7. additionalContext 怎样进入下一次模型请求
公开契约只保证补充内容会进入 Claude 的上下文。为了理解它为什么不是“修改 system prompt”,可以继续观察当前内部消息链:
1
2
3
4
5
6
7
Hook 结构化输出
→ 校验 hookEventName
→ 内部 hook_additional_context attachment
→ 包装成 <system-reminder> 文本
→ 转成 user role 下的模型可见内容
→ 与相邻 user 消息或 tool_result 规范化
→ 下一次 LLM API 调用读取
7.1 attachment 是内部中间表示
Claude Code 不必在收到 Hook 输出时就直接拼接最终 API Payload。它先保留一份带来源信息的内部对象,例如:
1
2
3
4
5
6
{
type: "hook_additional_context",
content: ["剩余时间不足,请开始收敛结论"],
hookName: "PostToolUse:Read",
hookEvent: "PostToolUse"
}
这样可以把事件处理与最终消息排版解耦。文件变化、Skill 引用和 Hook 提醒都可能经过 attachment 或类似的内部消息抽象,但它们的触发条件和语义来源仍然不同。
7.2 system-reminder 不是 API 顶层 system prompt
当前实现可能把补充内容包装成:
1
2
3
<system-reminder>
PostToolUse:Read hook additional context: 剩余时间不足,请开始收敛结论
</system-reminder>
这个标签表达“这是宿主注入的框架提醒”,不代表 Claude Code 修改了 Anthropic Messages API 的顶层 system 参数。
7.3 user role 不等于真人输入
工具结果本来就位于 API 的 user role。Hook 提醒也可能作为 user role 下的文本进入消息序列:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_123",
"content": "文件内容"
},
{
"type": "text",
"text": "<system-reminder>...</system-reminder>"
}
]
}
协议角色只描述消息在对话序列中的位置,不能据此判断内容来自真人,也不能据此推导它具有更高权限。
当前内部实现还可能把紧邻工具结果的提醒折叠进 tool_result.content,避免形成额外的对话边界。因此最稳妥的结论是:
additionalContext在语义上成为模型可见的补充文本;它在最终请求中是独立 text block,还是并入相邻 tool_result,属于版本相关的内部排版。
业务代码不应解析 <system-reminder> 的具体文本格式。
8. 输入、退出码与结构化输出
8.1 输入是由事件名判别的平铺 JSON
command Hook 从 stdin 读取一层平铺对象。公共字段与事件专属字段并排出现,通过 hook_event_name 区分事件:
1
2
3
4
5
6
7
8
9
10
{
"session_id": "8f2c1e6a",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/workspace",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "npm test" },
"tool_use_id": "toolu_123"
}
不同事件会替换专属字段。例如 UserPromptSubmit 提供 prompt,PostToolUseFailure 提供 error。
8.2 matcher 与 if 是粗筛和细筛
Hook 配置先按事件分组,再经过两层不同粒度的筛选:
matcher位于{matcher, hooks}这一层,决定整组 handler 是否参与当前事件。Tool Use 事件通常按tool_name粗筛,例如只处理 Bash;其他事件可能匹配各自的判别字段。省略 matcher 或使用*表示当前事件全部匹配,不表示跨越所有 Hook 事件。if位于单个 handler 上,使用权限规则语法同时匹配工具名与参数,例如Bash(rm *)。它在进程启动前细筛,可以避免每次 Bash 调用都启动脚本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git push --force*)",
"command": "~/scripts/check-force-push.sh"
},
{
"type": "command",
"if": "Bash(npm install*)",
"command": "~/scripts/check-npm-install.sh"
}
]
}]
}
}
一次 git push --force origin main 先以 tool_name: "Bash" 命中 matcher,再以具体命令命中第一条 if;第二条 handler 不会启动。两层关系是:
1
2
3
4
事件发生
→ matcher 判断这一组是否参与
→ if 判断组内哪一条 handler 真正运行
→ handler 读取完整输入并作最终决定
if 只适用于工具事件:PreToolUse、PostToolUse、PostToolUseFailure、PermissionRequest 和 PermissionDenied。放在其他事件上会导致该 handler 不运行。
它还有两个边界:
- 一条
if只能写一条权限规则,没有&&、||或列表语法;需要多条条件时应拆成多个 handler,或把复杂判断放进 handler; - Bash 中的变量、命令替换和复合命令会让静态判断变复杂。Claude Code 无法确定实际子命令时可能保守地运行 Hook,因此
if是减少无关 handler 启动的筛选器,不是硬安全边界。真正的允许与拒绝仍应由权限系统或 handler 的确定性校验完成。
8.3 退出码先决定输出怎样解释
command Hook 通过 stdout、stderr 和退出码通信:
| 退出码 | 基本语义 |
|---|---|
0 | 成功;若需要决策或注入模型上下文,应返回符合事件 schema 的 JSON |
2 | 阻止型错误;在该事件支持阻止时停止动作,并把 stderr 作为反馈 |
| 其他非零值 | 非阻止错误;通常记录错误并继续流程 |
最容易踩的坑是把 exit 1 当作拒绝。真正需要拒绝时,应使用 exit 2 或事件对应的结构化决策字段。
工具类 Hook 成功执行后的普通 stdout 不应被当成可靠的模型消息通道;当前官方文档将其视为调试日志。需要让模型读取内容时,明确返回 hookSpecificOutput.additionalContext。
8.4 通用 JSON 字段与事件字段不要混用
常见顶层字段包括:
| 字段 | 作用 |
|---|---|
continue | false 时终止 Claude 后续处理 |
stopReason | 终止时给用户看的原因 |
suppressOutput | 是否隐藏 Hook 的普通输出 |
systemMessage | 给用户显示的提示,不是模型上下文 |
hookSpecificOutput | 承载当前事件专属字段 |
hookSpecificOutput.hookEventName 必须与实际事件一致。additionalContext 也应放在该对象内部;放在 JSON 根部会被忽略。
9. 一个完整应用:时间预算控制
时间预算是三条通道协作的典型场景:
1
2
3
4
5
6
7
8
PostToolUse / PostToolUseFailure additionalContext
→ 告诉模型剩余时间,推动主动收敛
PreToolUse permissionDecision
→ 进入最终阶段后拒绝新工具,阻止继续扩张
任务级 deadline
→ 时间耗尽时强制终止请求
三层分别解决“提醒收敛”“禁止继续行动”和“最终截止”。它们不能互相替代:
- 只有
additionalContext,模型仍可能忽略提醒; - 只有 Pre Hook,无法打断已经运行的工具或正在进行的模型生成;
- 只有 deadline,Agent 可能在来不及整理结论时被直接终止。
成功和失败事件都应考虑。连续失败同样消耗预算;如果只监听 PostToolUse,Agent 可能在重试中持续耗时,却收不到新的时间反馈。
10. 设计边界
10.1 additionalContext 不是安全边界
它只是模型可见信息:
- 模型可能误解或忽略;
- 文本会占用上下文窗口;
- 高频重复提醒可能稀释真正重要的信息;
- 会话压缩和消息规范化可能改变最终排版。
危险操作应使用 Pre 的确定性决定,敏感信息应在数据边界脱敏,硬超时应由 deadline 执行。
10.2 Post Hook 不能撤销副作用
Post Hook 可以改变模型看到的结果,也可以追加解释,但工具已经执行。需要防止某件事发生,控制点必须前移到 Pre Hook、权限系统或沙箱。
10.3 不把内部消息结构当作公开协议
attachment 类型、<system-reminder> 文案、连续 user 消息合并和文本折叠都可能变化。外部集成应依赖官方 Hook 输入输出 schema,而不是解析 transcript 中的内部包装。
10.4 CLI 与 SDK 版本需要分别核验
当前 CLI 文档支持的事件或字段,不代表旧版 Agent SDK 的语言绑定已经暴露同名类型。集成时至少核对:
- Claude Code CLI 版本;
- Agent SDK 及语言绑定版本;
- 当前事件支持的输入输出字段;
- 工具输出的具体 schema;
- Hook 触发路径是否覆盖成功、失败和权限拒绝。
11. 测试重点
- Hook 是否注册到正确事件和 matcher;
hookEventName是否与事件一致;updatedInput是否真正传给本次工具;deny是否在副作用发生前阻止调用;- matcher 与
if是否分别命中了预期的工具类别和具体参数; - 多个 Pre Hook 冲突时,是否按
deny > defer > ask > allow合并,且测试没有依赖执行顺序; PermissionRequest是否只在真实权限请求路径触发;PermissionDenied的测试是否运行在 auto mode,并确认retry不会恢复已拒绝调用;- 并行工具是否逐个触发 Post Hook、整批结束后只触发一次
PostToolBatch; - Pre 的
additionalContext是否被误写成“执行前触发模型重推理”; - Post 替换结果是否满足工具输出 schema;
- 模型可见结果被替换后,是否有人误以为现实副作用也被撤销;
- 成功、执行失败、权限拒绝和用户中断是否进入预期路径;
- Hook 错误是否泄露凭据或未截断的大结果;
- 升级 CLI 或 SDK 后,关键字段和触发时序是否重新核验。
12. 复习入口
12.1 最短心智模型
1
2
3
4
5
6
7
8
9
模型生成 tool_use
↓
Pre 控制本次工具如何执行
↓
工具产生 Observation
↓
Post 控制结果如何反馈给模型
↓
下一次模型调用消费 tool_result 与 additionalContext
1
2
3
Pre 主要改变现实行为
Post 主要改变执行后的模型认知
additionalContext 只在下一次模型调用中被读取
12.2 易混点
- Observation 是工具反馈,下一次 API 调用负责运输和消费它;
tool_result位于 user role,不等于真人输入;- Pre 的上下文在执行前产生,但下一次模型调用才读取;
- 只返回 Pre
additionalContext不会自动阻止工具; - Post 可以替换模型可见结果,不能撤销现实副作用;
- matcher 先按事件判别字段粗筛,
if再按工具名和参数细筛; PermissionRequest处理即将发生的权限确认,PermissionDenied只处理 auto mode 已经发生的拒绝;PostToolBatch在整批工具结束后只触发一次,适合汇总而不是单工具反馈;- 多个 Pre Hook 都会执行,权限决定按
deny > defer > ask > allow合并; additionalContext是认知通道,不是权限或安全边界;<system-reminder>是当前内部包装,不等于 API 顶层 system prompt;- 普通 stdout 不是 Tool Use Hook 向模型注入内容的可靠通道;
exit 1不等于拒绝,真正阻止应使用exit 2或结构化决策;- 当前 CLI 文档不能倒推旧 SDK 已支持同一字段。
13. 核验入口
- Claude Code Hooks 官方参考
- Claude Code Hooks 指南
- Claude Code 上下文窗口说明
harness/claudecode/src/utils/hooks.ts:Hook 输出解析和决策处理;harness/claudecode/src/services/tools/toolHooks.ts:Tool Use Hook 调度和 attachment 生成;harness/claudecode/src/utils/messages.ts:attachment 到模型消息的规范化;harness/claudecode/src/query.ts:Agent turn 循环。