Agent X-Ray
RuntimeNotesAbout
Notes/源码拆解/Claude Code Harness/第11章

第11章:钩子系统 —— 27 个事件的治理层

13 分钟 · 更新于 2026-09-01

第11章:钩子系统 —— 27 个事件的治理层

src/utils/hooks.ts5022 行,是全仓最大的单文件。它实现了 27 个事件点 × 4 种钩子类型,钩子能改写工具输入、做权限决定、阻止循环继续、往上下文里注入内容。本章讲这套治理层的能力边界,以及一条本仓亲身踩过的坑。


一、27 个事件

ts
export const HOOK_EVENTS = [
  'PreToolUse', 'PostToolUse', 'PostToolUseFailure',
  'Notification', 'UserPromptSubmit',
  'SessionStart', 'SessionEnd',
  'Stop', 'StopFailure',
  'SubagentStart', 'SubagentStop',
  'PreCompact', 'PostCompact',
  'PermissionRequest', 'PermissionDenied',
  'Setup',
  'TeammateIdle', 'TaskCreated', 'TaskCompleted',
  'Elicitation', 'ElicitationResult',
  'ConfigChange',
  'WorktreeCreate', 'WorktreeRemove',
  'InstructionsLoaded', 'CwdChanged', 'FileChanged',
] as const

[源码 src/entrypoints/sdk/coreTypes.ts:24]

(数组里是 27 项,上面按语义分了行。)

按覆盖的生命周期分组:

事件覆盖什么
工具PreToolUse PostToolUse PostToolUseFailure一次工具调用的三个时刻
权限PermissionRequest PermissionDenied权限决策前后
轮次UserPromptSubmit Stop StopFailure用户提交、轮次结束、轮次失败结束
会话SessionStart SessionEnd Setup会话与初始化
子 agentSubagentStart SubagentStop子 agent 生命周期
压缩PreCompact PostCompact压缩前后
任务/团队TaskCreated TaskCompleted TeammateIdle任务系统与多 agent
交互Elicitation ElicitationResult NotificationMCP 的 elicitation 协议、通知
环境ConfigChange CwdChanged FileChanged InstructionsLoaded配置、工作目录、文件、CLAUDE.md 加载
worktreeWorktreeCreate WorktreeRemovegit worktree 隔离

对照 Codex 的 6 个事件点,这是数量级的差别。但数量不是重点,覆盖面才是——注意 InstructionsLoaded(CLAUDE.md 被加载时)、CwdChanged(工作目录变了)、ConfigChange(配置变了)这几个:它们让钩子可以对运行时环境的变化做出反应,而不只是对 agent 的动作做出反应。


二、四种钩子类型

这是 Claude Code 钩子系统和其它 harness 差别最大的地方 [源码 src/schemas/hooks.ts:31-170]:

2.1 command —— 跑一条 shell 命令

ts
{
  type: 'command',
  command: string,
  if?: string,          // 权限规则语法的过滤条件
  shell?: 'bash' | 'powershell' | …,
  timeout?: number,     // 秒
  statusMessage?: string,   // spinner 里显示的自定义文案
  once?: boolean,       // 跑一次就自我移除
  async?: boolean,      // 后台跑,不阻塞
  asyncRewake?: boolean // 后台跑,退出码 2 时唤醒模型(隐含 async)
}

asyncRewake 很有意思:后台跑,如果失败了(退出码 2 = 阻塞错误)就把模型叫醒。这是"跑个 lint,出问题再说"这类场景的原生支持。

2.2 prompt —— 用一个 LLM 判断

ts
{
  type: 'prompt',
  prompt: string,       // 'Use $ARGUMENTS placeholder for hook input JSON.'
  model?: string,       // 不指定就用 small fast model
  if?: string,
  timeout?: number,
  …
}

钩子本身可以是一次模型调用。 输入 JSON 通过 $ARGUMENTS 占位符注入。

2.3 agent —— 一个完整的验证 agent

ts
{
  type: 'agent',
  prompt: string,       // 'Prompt describing what to verify
                        //  (e.g. "Verify that unit tests ran and passed.")'
  model?: string,       // 默认 Haiku
  timeout?: number,     // 默认 60 秒
  …
}

注释叫它 Agentic verifier hook type它不是判断一次,是跑一个能用工具的 agent 去验证。

这个 schema 上有一段很实在的事故注释:

ts
// DO NOT add .transform() here. This schema is used by parseSettingsFile,
// and updateSettingsForSource round-trips the parsed result through
// JSON.stringify — a transformed function value is silently dropped,
// deleting the user's prompt from settings.json (gh-24920, CC-79).

Zod 的 .transform() 把字符串变成了函数,然后配置回写时 JSON.stringify 把函数丢了——用户 settings.json 里的 prompt 凭空消失。 两个 issue 编号。

可迁移的判断 ㉓ 用于配置文件解析的 schema 不要带 .transform(),除非你确认配置永远不会被程序回写。

这是个很容易踩的坑:解析时的便利转换,在"读 → 改一个字段 → 写回"的往返里会静默丢数据。判断方法很简单——问一句"这个 schema 解析出来的东西会不会被 stringify 回去"

2.4 http —— POST 到一个 URL

ts
{
  type: 'http',
  url: string,
  headers?: Record<string, string>,
  allowedEnvVars?: string[],
  …
}

headers 里可以用 $VAR_NAME 引用环境变量,但有个强制约束:

Only variables listed in allowedEnvVars will be interpolated. … Only variables listed here will be resolved; all other $VAR references are left as empty strings. Required for env var interpolation to work.

默认不插值任何环境变量,必须显式列出白名单。 否则一个恶意的(或粗心的)钩子配置就能把 $AWS_SECRET_ACCESS_KEY 发到任意 URL。

这是整个钩子系统里最重要的一处安全设计,而且它是 fail-closed 的:不写白名单 → 变量解析成空串 → 请求失败 → 用户去看文档,而不是悄悄泄漏。


三、if 条件:不匹配就不 spawn

ts
const IfConditionSchema = z.string().optional().describe(
  'Permission rule syntax to filter when this hook runs (e.g., "Bash(git *)"). ' +
  'Only runs if the tool call matches the pattern. Avoids spawning hooks for non-matching commands.')

[源码 src/schemas/hooks.ts:19]

用的是权限规则的语法Bash(git *)Read(*.ts)),复用第 10 章那套匹配器。

这就是第 8 章那个 preparePermissionMatcher 的用途:

ts
/**
 * Prepare a matcher for hook `if` conditions (permission-rule patterns like
 * "git *" from "Bash(git *)"). Called once per hook-input pair; any
 * expensive parsing happens here. Returns a closure that is called per
 * hook pattern.
 */

两阶段设计的原因:一个工具调用可能要匹配几十条钩子的 if,而 bash 命令的解析(拆 token、处理引号)很贵。所以解析一次,然后用返回的闭包去匹配每个模式。

注释里那句 "Avoids spawning hooks for non-matching commands" 点明了 if 的价值:不是"跑了再判断",是根本不启动进程。对于每次工具调用都会触发的钩子,这是数量级的差别。


四、钩子能返回什么:18 个字段

ts
export interface HookResult {
  message?: HookResultMessage
  systemMessage?: string
  blockingError?: HookBlockingError
  outcome: 'success' | 'blocking' | 'non_blocking_error' | 'cancelled'
  preventContinuation?: boolean
  stopReason?: string
  permissionBehavior?: 'ask' | 'deny' | 'allow' | 'passthrough'
  hookPermissionDecisionReason?: string
  additionalContext?: string
  initialUserMessage?: string
  updatedInput?: Record<string, unknown>
  updatedMCPToolOutput?: unknown
  permissionRequestResult?: PermissionRequestResult
  elicitationResponse?: ElicitationResponse
  watchPaths?: string[]
  elicitationResultResponse?: ElicitationResponse
  retry?: boolean
  hook: HookCommand | HookCallback | FunctionHook
}

[源码 src/utils/hooks.ts:338]

按能力分类:

能力字段影响
改写输入updatedInput工具拿到的参数变了
改写输出updatedMCPToolOutputMCP 工具的结果变了
权限决定permissionBehavior + hookPermissionDecisionReason第 9 章那张真值表
阻断blockingError / preventContinuation / stopReason工具不执行 / 循环不继续
注入additionalContext / systemMessage / initialUserMessage往上下文里塞东西
交互elicitationResponse / permissionRequestResult代替用户回答
重试retry告诉模型可以再试一次
监听watchPaths声明要监视哪些路径

四种能力的组合让钩子成了真正的策略层,而不是通知机制。

outcome 那四个值是本章后半段的关键:success / blocking / non_blocking_error / cancelled

4.1 钩子的输出协议:JSON 或纯文本

ts
function parseHookOutput(stdout: string) {
  const trimmed = stdout.trim()
  if (!trimmed.startsWith('{')) {
    logForDebugging('Hook output does not start with {, treating as plain text')
    return { plainText: stdout }
  }
  const result = validateHookJson(trimmed)
  if ('json' in result) return result
  // For command hooks, include the schema hint in the error message
  const errorMessage = `${result.validationError}\n\nExpected schema:\n${jsonStringify({
    continue: 'boolean (optional)',
    suppressOutput: 'boolean (optional)',
    stopReason: 'string (optional)',
    …
  })}`
}

[源码 src/utils/hooks.ts:398]

判别方式是"第一个非空字符是不是 {"。 简单粗暴但够用——钩子作者不需要声明自己输出什么格式。

JSON 校验失败时,错误消息里附上完整的期望 schema。又是第 9 章那条"错误消息要回答该怎么办"。


五、信任门:所有钩子都要先过

ts
/**
 * Historical vulnerabilities that prompted this check:
 * - SessionEnd hooks executing when user declines trust dialog
 * - SubagentStop hooks executing when subagent completes before trust
 */
export function shouldSkipHookDueToTrust(): boolean {
  // In non-interactive mode (SDK), trust is implicit - always execute
  if (getIsNonInteractiveSession()) return false
  // In interactive mode, ALL hooks require trust
  return !checkHasTrustDialogAccepted()
}

[源码 src/utils/hooks.ts:286]

背景:你在一个陌生仓库里启动 Claude Code,它会问"信任这个目录里的文件吗?"。如果你选不信任,那么这个仓库 .claude/settings.json 里的钩子不应该执行——因为钩子就是任意代码执行。

注释列的两个历史漏洞很典型:

  • SessionEnd:用户点了"不信任"→ 会话结束 → SessionEnd 钩子跑了
  • SubagentStop:子 agent 在信任对话框弹出之前就跑完了 → SubagentStop 钩子跑了

两个漏洞的共同点是"生命周期事件在信任决定之前或之后发生"。 解法是一刀切:交互模式下所有钩子都过这道门。

而非交互模式(SDK)默认信任——因为那时候是程序在调用,没有"用户还没决定"这个状态。


六、失败语义:本仓踩过的那个坑

现在可以解释第 6 章那 2249 条 hook_non_blocking_error 了。

6.1 钩子的四种 outcome 对应四种附件

第 6 章列过钩子独占 10 个附件类型,其中最相关的四个:

outcome附件类型行为
successhook_success正常
blockinghook_blocking_error工具不执行,错误进上下文
non_blocking_errorhook_non_blocking_error工具照常执行,错误进上下文
cancelledhook_cancelled被取消

non_blocking_error 的语义是"钩子自己出错了,但不影响工具"。 这是对的默认值——一个 lint 钩子挂了,不该让所有文件编辑都失败。

但"不影响工具"不等于"没有代价":每次都会往上下文里加一条附件。

6.2 本仓的实测

[实测] 本机 ~/.claude/projects/-home-chenkun-variFlight-work-VariFlightWork/ 下 48 个会话,hook_non_blocking_error 附件 2249 条——是所有附件类型里最多的一种,占全部附件的 64%

来源是本仓 .claude/settings.json 里注册的四个 PowerShell 钩子,命令全部长这样:

text
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$CLAUDE_PROJECT_DIR/.claude/hooks/X.ps1"

错误信息 2249 条完全一致

text
Failed with non-blocking status code: /bin/sh: 行 1: powershell.exe: 未找到命令

根因不是"Linux 没有 PowerShell" 本机 ~/.local/bin/pwsh 是装了的(PowerShell 7.6.5),缺的只是 Windows 那个可执行文件名 powershell.exe。四个 .ps1 拿到 Linux 的 pwsh 下逐个实测,全部 exit 0、行为正确,包括中文路径和中文内容。

所以这不是"脚本不跨平台",是可执行文件名写死了。这个区别决定了修法的难易——前者要重写四个脚本,后者只要改四个命令字符串。

各钩子的贡献很反直觉:

钩子matcher条数为什么是这个量
log-codescope.ps1Bash|PowerShell1472每跑一条 bash 命令就失败一次
inject-dir-claude-md.ps1Write|Edit|MultiEdit|Read|NotebookEdit334每次读写文件
sync-agents-md.ps1Write|Edit|MultiEdit244每次写文件
guard-windows-encoding.ps1Write|Edit|MultiEdit198同上(有 timeout: 15,失败得更快)

log-codescope 一个占了三分之二——它挂在 Bash 上,而 agent 跑 bash 的频率远高于写文件。"哪个钩子最费上下文"取决于 matcher 的触发频率,和这个钩子本身重不重要无关。

于是在 130 这台 Arch Linux 上:

text
每次 Bash / Read / Write / Edit
    → 触发对应的 PreToolUse / PostToolUse 钩子
    → powershell.exe 找不到
    → outcome: 'non_blocking_error'
    → 一条 hook_non_blocking_error 附件进上下文
    → 工具照常执行(功能没坏)

功能上完全正常,代价是每次工具调用都往上下文里倒一条垃圾。

而本仓 CLAUDE.md 里原本那条规则:

Rule (Linux/macOS): the PowerShell hook does not fire there — before the session's first Read/Write/Edit under such a sub-directory, Read that directory's CLAUDE.md chain (directory → project root) manually.

它解决的是功能缺失(Linux 上目录级 CLAUDE.md 不会被自动注入)。但它没有提到上下文开销这一面——而实测数据显示,这才是更大的那个代价。

后续:2026-08-25 已修复 四个命令改成运行时解析解释器:

sh
PS="$(command -v powershell.exe || command -v pwsh || true)"; [ -z "$PS" ] && exit 0; \
  exec "$PS" -NoProfile -ExecutionPolicy Bypass -File "$CLAUDE_PROJECT_DIR/.claude/hooks/X.ps1"

Windows 走 powershell.exe(行为完全不变),Linux/macOS 走 pwsh,两个都没有则静默 exit 0——"没有解释器"从错误降级成了 no-op。改完之后 CLAUDE.md 里那条 Linux 手动读取规则也同步改成了"需要 PATH 上有 pwsh,没有才回退手动"。

顺带说明这套配置一直依赖 Git Bash:命令里的 "$CLAUDE_PROJECT_DIR/..." 是 POSIX 展开,在 PowerShell 里会展开成空——所以改造用 POSIX 语法没有引入新的平台假设。

可迁移的判断 ㉔ 跨平台的钩子命令不要硬编码可执行文件名,改成运行时解析 + 找不到就静默 no-op。

三个要点:(a) 保留原平台的优先级(powershell.exe 在前,Windows 行为一字不变);(b) 找不到解释器时 exit 0 而不是让 && 链失败——"这台机器没装"不该被记成错误;(c) 但脚本自身失败仍要如实上报,所以用 exec 让退出码透传。

自查方法(第 6 章给过脚本):统计自己 transcript 里的附件分布。如果某一类附件的条数远超其它类型,那就是一个正在持续消耗上下文的问题——而且要按 matcher 频率而不是按"钩子重要性"去找元凶。

更一般的原则:"非阻塞失败"不等于"零成本失败"。 在一个上下文即预算的系统里,任何会重复产生记录的失败都是持续的成本。

6.3 修法:四个候选,为什么选第四个

方案结果
① 加 if 条件把它限制在能跑的平台走不通——if 用的是权限规则语法(匹配工具名和参数),不匹配平台
② 把 .ps1 改写成 Python / Node可行但过重:脚本本身没问题,实测在 pwsh 下全部正常,重写等于制造两份要同步的实现
③ 按平台分设置层(Windows 留 settings.json,Linux 写 settings.local.json半可行:settings.local.json 的 hooks 和 settings.json合并不是覆盖,所以坏的那份还是会跑;要真生效得先从入库文件里删掉,Windows 那边就得各自重新配
命令里运行时解析解释器选它——四个字符串各改一次,Windows 行为一字不变,Linux 立刻生效

最终写法:

sh
PS="$(command -v powershell.exe || command -v pwsh || true)"; [ -z "$PS" ] && exit 0; \
  exec "$PS" -NoProfile -ExecutionPolicy Bypass -File "$CLAUDE_PROJECT_DIR/.claude/hooks/X.ps1"

三处细节都是必要的:

  • || true —— 两个都找不到时 command -v 返回非零,不吞掉的话整条命令就以失败告终,等于没改
  • [ -z "$PS" ] && exit 0 —— 把"这台机器没装解释器"从错误降级成 no-op。这是本章的核心教训:不该被记成失败的事情就别让它失败
  • exec —— 让脚本自己的退出码透传上去。降级的是"找不到解释器",不是"脚本跑挂了"——后者仍然要如实上报,否则就从"倒垃圾"变成了"藏错误"

[实测] 改完之后sync-agents-md 在 Linux 上第一次成功触发并重新生成了 AGENTS.mdinject-dir-claude-md 正常注入了目录级 CLAUDE.md,两分多钟的密集 Edit/Bash 操作里新增 hook_non_blocking_error 为 0(同期按原速率本该产生十几条)。配置改动被 settings watcher 当场热加载,不需要重启会话。


七、超时与并发

ts
const TOOL_HOOK_EXECUTION_TIMEOUT_MS = 10 * 60 * 1000   // 10 分钟
const SESSION_END_HOOK_TIMEOUT_MS_DEFAULT = 1500        // 1.5 秒

[源码 src/utils/hooks.ts:166, 175]

工具钩子默认 10 分钟,会话结束钩子默认 1.5 秒。 差了 400 倍。

理由不难推:会话结束时用户已经在等着退出了,钩子跑太久就是卡住不让走。而工具钩子可能是在跑测试、跑构建。

SessionEnd 的超时还可以用环境变量覆盖(CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS)。

并发:第 9 章那个"用墙钟时间不是耗时之和"的注释已经说明了——同一个事件的多个钩子是并行跑的

ts
// Emit PreToolUse summary immediately so it's visible while the tool executes.
// Use wall-clock time (not sum of individual durations) since hooks run in parallel.

八、钩子的输入:每个钩子都知道自己在哪

ts
export function createBaseHookInput(permissionMode?, sessionId?, agentInfo?) {
  return {
    session_id: resolvedSessionId,
    transcript_path: getTranscriptPathForSession(resolvedSessionId),
    cwd: getCwd(),
    permission_mode: permissionMode,
    agent_id: agentInfo?.agentId,
    agent_type: resolvedAgentType,
  }
}

[源码 src/utils/hooks.ts:301]

六个字段是所有钩子共有的。两个观察:

transcript_path —— 钩子可以直接读整个对话记录。这是个很大的能力:一个钩子可以基于"这次会话到目前为止发生了什么"来做决定,而不只是基于当前这次工具调用。

第 2 章讲过 getTranscriptPathForSession 有个很长的注释解释它为什么要同时看 sessionProjectDir

Without this, hooks get a transcript_path computed from originalCwd while the actual file was written to sessionProjectDir (set by switchActiveSession on resume/branch) — different directories, so the hook sees MISSING (gh-30217).

/resume 之后钩子拿到的路径指向一个不存在的文件。 一个 issue 编号。

agent_id + agent_type ——

ts
// agent_type: subagent's type (from toolUseContext) takes precedence over
// the session's --agent flag. Hooks use agent_id presence to distinguish
// subagent calls from main-thread calls in a --agent session.

钩子靠 agent_id 是否存在来判断"这是主线程还是子 agent"。 因为在 --agent xxx 启动的会话里,主线程也有 agent_type,只有 agent_id 能区分。


九、钩子在整个系统里的位置

把前面几章串起来,钩子的干预点是这样的:

text
用户输入
  ↓ UserPromptSubmit ────────── 可改写输入 / 阻断
主循环开始
  ↓ SessionStart(首轮)
模型请求 ───────────────────── PostSampling(第 3 章,异步)
  ↓
工具调用
  ↓ PreToolUse ───────────────  改输入 / 权限决定 / 阻断 / 注入上下文
  ↓ PermissionRequest ────────  权限决定
  ↓ [权限系统]
  ↓ PermissionDenied ─────────  可要求重试
  ↓ [tool.call()]
  ↓ PostToolUse / PostToolUseFailure ── 改 MCP 输出 / 注入 / 阻断继续
  ↓
轮次结束
  ↓ Stop ─────────────────────  可以拒绝让轮次结束(塞一条继续指令)
  ↓ StopFailure(API 错误时)
  ↓
压缩
  ↓ PreCompact ───────────────  可以追加压缩指令(mergeHookInstructions)
  ↓ PostCompact
  ↓
子 agent
  ↓ SubagentStart / SubagentStop

能改变控制流的有五个UserPromptSubmitPreToolUsePostToolUseStopPermissionRequest

Stop 那个尤其强——第 3 章讲过,Stop 钩子返回阻塞错误会让主循环 continue,塞一条消息让模型接着干。这是"agent 自动接着干活"的机制来源。

PreCompact 能追加压缩指令 [源码 src/services/compact/compact.ts:376]:

ts
/**
 * Merges user-supplied custom instructions with hook-provided instructions.
 * User instructions come first; hook instructions are appended.
 */
export function mergeHookInstructions(userInstructions, hookInstructions)

一个项目可以配一个 PreCompact 钩子,让每次压缩都保留特定的东西(比如"重点保留 TypeScript 的类型改动")。第 7 章那份压缩提示词末尾就留了这个口子:

There may be additional summarization instructions provided in the included context. If so, remember to follow these instructions when creating the above summary.


十、动手复核

bash
cd claude-code-deep-dive/extracted-source

# 1. 27 个事件
sed -n '24,52p' src/entrypoints/sdk/coreTypes.ts

# 2. 四种钩子类型的完整 schema
sed -n '17,216p' src/schemas/hooks.ts

# 3. HookResult 的 18 个字段
sed -n '336,380p' src/utils/hooks.ts

# 4. 信任门与它的两个历史漏洞
sed -n '272,300p' src/utils/hooks.ts

# 5. 超时
grep -n 'TOOL_HOOK_EXECUTION_TIMEOUT_MS\s*=\|SESSION_END_HOOK_TIMEOUT_MS_DEFAULT\s*=' src/utils/hooks.ts

# 6. 输出协议(JSON or 纯文本)
sed -n '380,430p' src/utils/hooks.ts

# 7. 那条 .transform() 事故注释
grep -n -B3 -A10 'DO NOT add .transform' src/schemas/hooks.ts

# 8. 环境变量白名单(最重要的安全设计)
grep -n -A6 'allowedEnvVars' src/schemas/hooks.ts

本机侧 [实测] 自查你的钩子有没有在偷偷倒垃圾:

bash
D=~/.claude/projects/$(pwd | sed 's#/#-#g')
python3 -c "
import json, collections, glob, sys
c = collections.Counter()
for f in glob.glob(sys.argv[1] + '/*.jsonl'):
    for line in open(f):
        try: d = json.loads(line)
        except: continue
        if d.get('type') == 'attachment':
            t = (d.get('attachment') or {}).get('type')
            if t and t.startswith('hook_'): c[t] += 1
print(dict(c.most_common()))
" "$D"

hook_non_blocking_error 数字很大 = 有钩子在持续失败。


十一、总结

  1. 27 个事件,覆盖工具、权限、轮次、会话、子 agent、压缩、任务/团队、交互、环境、worktree 十组;对照 Codex 的 6 个是数量级差别
  2. 四种钩子类型:shell 命令、LLM prompt、完整的验证 agent、HTTP POST。钩子本身可以是一次模型调用甚至一个 agent
  3. if 条件用权限规则语法,复用同一套匹配器;两阶段匹配器(贵的解析做一次);不匹配就不启动进程
  4. 钩子能返回 18 个字段,覆盖改写输入、改写 MCP 输出、权限决定、阻断、注入上下文、代替用户交互、要求重试七类能力
  5. HTTP 钩子的环境变量必须显式白名单,否则解析成空串——fail-closed 的密钥防泄漏设计
  6. 信任门:交互模式下所有钩子都要先过信任对话框;两个历史漏洞都是"生命周期事件发生在信任决定之外"
  7. non_blocking_error 不阻塞工具,但每次都往上下文塞一条附件——[实测] 本仓在 Linux 上因四个 PowerShell 钩子硬编码 powershell.exe 产生了 2249 条,占全部附件的 64%;元凶按 matcher 触发频率排序(挂 Bash 的那个占三分之二),已于 2026-08-25 用运行时解析解释器修复
  8. 超时差 400 倍:工具钩子 10 分钟,会话结束钩子 1.5 秒
  9. 每个钩子都拿到 transcript_path,可以基于整个会话历史做决定;靠 agent_id 是否存在区分主线程和子 agent
  10. 五个事件能改变控制流UserPromptSubmit / PreToolUse / PostToolUse / Stop / PermissionRequestStop 拒绝结束就是"自动接着干"的机制来源
  11. .transform() 不能用在配置 schema 上——回写时 JSON.stringify 会静默丢掉函数值,删掉用户的配置

下一章进入多 agent:fork 和 fresh subagent 到底差在哪,以及为什么 fork 便宜。


  • 第10章-权限模型-六种模式与被外包的沙箱
  • 第12章-Agent调度-fork与fresh两条路
  • 第6章-附件与system-reminder-第二条注入通道 —— 那 2249 条附件的统计出处
  • 第9章-工具执行链-一次调用要过多少道关 —— PreToolUse 的七种结果与权限真值表
  • 第7章-上下文压缩-五层防线 —— PreCompact 追加压缩指令的落点
  • Codex 教程第 13 章 —— 6 个钩子事件的对照

本章目录
一、27 个事件二、四种钩子类型三、if 条件:不匹配就不 spawn四、钩子能返回什么:18 个字段五、信任门:所有钩子都要先过六、失败语义:本仓踩过的那个坑七、超时与并发八、钩子的输入:每个钩子都知道自己在哪九、钩子在整个系统里的位置十、动手复核十一、总结Related Documents
苏ICP备2025204887号-2