目录
  1. 1. 一、系统定位
  2. 2. 二、27 个钩子事件(完整表)
    1. 2.1. 工具生命周期(4 个)
    2. 2.2. 会话生命周期(4 个)
    3. 2.3. 子 Agent 生命周期(2 个)
    4. 2.4. 压缩生命周期(2 个)
    5. 2.5. 用户交互(3 个)
    6. 2.6. MCP Elicitation(2 个)
    7. 2.7. 文件与目录(3 个)
    8. 2.8. 多 Agent 任务(3 个)
    9. 2.9. 配置与指令(2 个)
    10. 2.10. 仓库维护(1 个)
  3. 3. 三、5 种钩子类型
    1. 3.1. 1. command(最常用)
    2. 3.2. 2. prompt(LLM 单轮)
    3. 3.3. 3. agent(LLM 多轮)
    4. 3.4. 4. http(HTTP POST)
    5. 3.5. 5. function(TypeScript 回调,仅会话作用域)
  4. 4. 四、配置源与优先级
  5. 5. 五、SessionHooks:会话级动态钩子
    1. 5.1. 存储结构
    2. 5.2. 操作 API
    3. 5.3. Function 钩子 vs Command 钩子
  6. 6. 六、AsyncHookRegistry:异步钩子注册表
    1. 6.1. 异步协议
    2. 6.2. 核心数据结构
    3. 6.3. 轮询与清理
  7. 7. 七、SSRF 防护(HTTP 钩子专用)
    1. 7.1. 阻断的 IP 范围
    2. 7.2. 防 DNS 重绑定
    3. 7.3. IPv4-mapped IPv6 防绕过
  8. 8. 八、Hook 事件广播系统
    1. 8.1. 事件类型
    2. 8.2. 广播策略
    3. 8.3. Pending Buffer
  9. 9. 九、Hooks 配置快照机制
  10. 10. 十、面试必考点
    1. 10.1. Q1:Claude Code 的钩子系统如何工作?能实现哪些能力?
    2. 10.2. Q2:exit code 协议是什么?
    3. 10.3. Q3:5 种钩子类型各有什么适用场景?
    4. 10.4. Q4:SessionHooks 为什么用 Map 而不是 Record?
    5. 10.5. Q5:HTTP 钩子的安全机制是什么?
    6. 10.6. Q6:异步钩子如何不阻塞主流程?
    7. 10.7. Q7:企业如何锁定钩子?
  11. 11. 涉及源文件
【Claude Code源码剖析】29-User Hooks 用户钩子系统

⚠️ 学习声明:本文档基于 Claude Code 2.1.88 源码分析整理,仅供个人学习研究使用,不做任何商业用途。


一、系统定位

Hooks 是 Claude Code 可扩展性的核心机制:用户(和企业管理员)可以在 Agent 生命周期的任意关键节点注入自定义逻辑,而不需要修改 CC 本身。

三类用途:

  1. 观察:记录工具调用日志、发送通知
  2. 拦截:阻止危险操作(exit 2 blocking)
  3. 增强:向 Claude 注入额外上下文(exit 0 stdout)

二、27 个钩子事件(完整表)

来源:src/utils/hooks/hookEvents.tsgetHookEventMetadata 函数)

工具生命周期(4 个)

事件 触发时机 支持 matcher 阻断能力
PreToolUse 工具调用前 tool_name ✅ exit 2 阻止工具执行
PostToolUse 工具调用后(成功) tool_name ⚠️ exit 2 向 model 发 stderr
PostToolUseFailure 工具调用失败后 tool_name ⚠️ exit 2 向 model 发 stderr
PermissionDenied 自动模式分类器拒绝工具 tool_name 可输出 retry:true 让 model 重试

PreToolUse exit 协议

  • exit 0:静默通过,stdout/stderr 不显示
  • exit 2:向 model 发 stderr 并阻止 工具执行
  • 其他:向用户显示 stderr,继续执行工具

PostToolUse exit 协议

  • exit 0:stdout 显示在 transcript 模式(Ctrl+O)
  • exit 2:立即向 model 发 stderr(可影响下一轮)
  • 其他:向用户显示 stderr

会话生命周期(4 个)

事件 触发时机 matcher 特殊行为
SessionStart 新会话启动 source(startup/resume/clear/compact) exit 0 stdout 注入 Claude;始终广播(SDK always-emit)
SessionEnd 会话结束前 reason(clear/logout/prompt_input_exit/other) 无阻断
Stop Claude 准备结束回复前 exit 2 继续对话(让 Claude 再想想)
StopFailure API 错误结束 turn error(rate_limit/auth 等) Fire-and-forget,输出全忽略

子 Agent 生命周期(2 个)

事件 触发时机 matcher exit 0 行为
SubagentStart Agent 工具调用启动 agent_type stdout 注入子 Agent
SubagentStop 子 Agent 结束前 agent_type exit 2 让子 Agent 继续运行

压缩生命周期(2 个)

事件 触发 matcher 阻断
PreCompact 上下文压缩前 trigger(manual/auto) exit 0 stdout 追加自定义压缩指令;exit 2 阻止压缩
PostCompact 压缩后 trigger 无阻断,stdout 显示给用户

用户交互(3 个)

事件 触发 阻断
UserPromptSubmit 用户提交 prompt exit 2 阻止处理并清除原始 prompt;exit 0 stdout 注入 Claude
Notification 发送通知时 notification_type(permission_prompt/idle_prompt 等)
PermissionRequest 权限对话框弹出时 输出 JSON hookSpecificOutput.decision 可程序化允许/拒绝

MCP Elicitation(2 个)

事件 触发 特殊输出
Elicitation MCP 服务端请求用户输入 输出 hookSpecificOutput.action(accept/decline/cancel)可自动响应
ElicitationResult 用户响应 MCP elicitation 后 exit 2 阻止响应(变为 decline)

文件与目录(3 个)

事件 触发 特殊机制
CwdChanged 工作目录变更 CLAUDE_ENV_FILE 注入环境变量;可返回 watchPaths 注册文件监控
FileChanged 被监控文件变化 matcher 是文件名模式(如 .envrc|.env);同样支持 CLAUDE_ENV_FILE
WorktreeCreate / WorktreeRemove Worktree 创建/删除 stdout 必须是绝对路径(Create);无阻断(Remove)

多 Agent 任务(3 个)

事件 触发 阻断
TeammateIdle Teammate 即将空闲 exit 2 向 teammate 发 stderr 并阻止空闲
TaskCreated 创建任务时 exit 2 阻止任务创建
TaskCompleted 标记任务完成时 exit 2 阻止任务完成

配置与指令(2 个)

事件 触发 阻断
ConfigChange 会话期间配置文件变更 exit 2 阻止变更应用
InstructionsLoaded CLAUDE.md 或规则文件加载 仅可观察,不支持阻断

仓库维护(1 个)

事件 触发 matcher
Setup init 或 maintenance 触发 trigger(init/maintenance);始终广播

三、5 种钩子类型

来源:src/utils/settings/types.tsHookCommand)、src/utils/hooks/hooksSettings.tsisHookEqual

1. command(最常用)

{
"type": "command",
"command": "python3 /path/to/pre_tool_use.py",
"shell": "bash",
"timeout": 30,
"if": "Bash(git *)"
}
  • shell:bash(默认)、powershell、powershell_core、sh、zsh、fish
  • if:条件过滤,格式 ToolName(pattern),仅在模式匹配时执行
  • timeout:秒(默认 10 分钟,与工具钩子超时一致)
  • 输入通过 stdin 接收 JSON

执行链路:hooks.ts:runHook()spawn() → 读 stdout/stderr → 解析 exit code

2. prompt(LLM 单轮)

{
"type": "prompt",
"prompt": "检查这段代码是否安全:$ARGUMENTS",
"model": "claude-haiku-4-5-20251001",
"timeout": 30
}
  • $ARGUMENTS 占位符在执行时替换为钩子 JSON 输入(addArgumentsToPrompt
  • 默认使用 getSmallFastModel()(Haiku)
  • 单次 queryModelWithoutStreaming() 调用,默认 30s 超时
  • 响应必须是 JSON:{"ok": true}{"ok": false, "reason": "..."}
  • ok=false 时等价于 exit 2(blocking)

实现:src/utils/hooks/execPromptHook.ts

3. agent(LLM 多轮)

{
"type": "agent",
"prompt": "验证 Agent 完成了计划中的所有步骤:$ARGUMENTS",
"model": "claude-haiku-4-5-20251001",
"timeout": 60
}
  • 使用完整 query() 引擎,最多 50 轮对话
  • 可访问所有工具(除 ALL_AGENT_DISALLOWED_TOOLS:不能启动子 Agent 或进入 PlanMode)
  • 通过 SyntheticOutputTool 返回结构化输出 {ok, reason?}
  • 自动注入 transcript 路径权限,Agent 可读取对话历史
  • 未在 50 轮内返回结果 → outcome: ‘cancelled’(不报错)
  • 分析事件:tengu_agent_stop_hook_*

实现:src/utils/hooks/execAgentHook.ts

4. http(HTTP POST)

{
"type": "http",
"url": "https://policy.internal/pre-tool-use",
"headers": {
"Authorization": "Bearer $MY_TOKEN"
},
"allowedEnvVars": ["MY_TOKEN"],
"timeout": 30
}
  • POST JSON 到指定 URL,body = 钩子输入 JSON
  • header 环境变量插值$VAR / ${VAR} → 仅 allowedEnvVars 中的变量被展开,防止 secrets 泄露
  • SSRF 防护(见下节)
  • 企业策略:allowedHttpHookUrls(URL 白名单)+ httpHookAllowedEnvVars(全局 env 变量白名单)
  • 禁用 axios 自动重定向(maxRedirects: 0)

实现:src/utils/hooks/execHttpHook.ts

5. function(TypeScript 回调,仅会话作用域)

addFunctionHook(setAppState, sessionId, 'PreToolUse', 'Bash', 
async (messages, signal) => {
// 返回 true 通过,false 阻断
return !isSomeCondition(messages)
},
'条件不满足,阻止执行',
{ timeout: 5000 }
)
  • 不可持久化:仅存 AppState 内存,会话结束即清除
  • 有返回 ID,可通过 removeFunctionHook(id) 移除
  • 超时默认 5s(比 command 短得多)
  • 无法通过 settings.json 配置,只能 SDK 编程注入

四、配置源与优先级

来源:src/utils/hooks/hooksSettings.ts:getAllHooks()

优先级(高 → 低):
userSettings (~/.claude/settings.json)
projectSettings (.claude/settings.json)
localSettings (.claude/settings.local.json)
policySettings (企业管理策略,不在 getAllHooks 但会覆盖一切)
pluginHook (插件注册的钩子,最低优先级 = 999)
sessionHook (内存临时,动态注入,与上述并列)
builtinHook (CC 内部,ANT-only)

去重机制:多个源指向同一个 settings.json 文件时(如在 home 目录运行,userSettings = projectSettings),通过 seenFiles: Set<string> 跳过重复文件。

企业锁定policySettings.allowManagedHooksOnly = true 时,userSettings/projectSettings/localSettings 的钩子全部忽略,只有管理员定义的 hooks 生效。


五、SessionHooks:会话级动态钩子

来源:src/utils/hooks/sessionHooks.ts

存储结构

type SessionHooksState = Map<string, SessionStore>
// sessionId per-session state

type SessionStore = {
hooks: {
[event in HookEvent]?: SessionHookMatcher[]
}
}

为什么用 Map 而不是 Record?

注释原话:高并发场景下,parallel() 启动 N 个 schema-mode agents 时,N 个 addFunctionHook 在同一个同步 tick 触发。Record + spread 是 O(N²)(每次 spread 复制增长的 Map)且触发所有 ~30 个 store listeners;Map.set() 是 O(1),返回 prev 不触发 listener。

操作 API

// 添加命令钩子(临时)
addSessionHook(setAppState, sessionId, 'PreToolUse', 'Bash', commandHook)

// 添加函数钩子(返回 ID 用于移除)
const id = addFunctionHook(setAppState, sessionId, 'PreToolUse', 'Bash', callback, errorMsg)

// 移除函数钩子
removeFunctionHook(setAppState, sessionId, 'PreToolUse', id)

// 清除会话所有钩子(session end 时调用)
clearSessionHooks(setAppState, sessionId)

Function 钩子 vs Command 钩子

维度 function command/prompt/agent/http
执行位置 进程内(TypeScript 回调) 子进程或外部 HTTP
持久化 ❌ 仅内存 ✅ settings.json
超时 5s 默认 30s-60s 默认
并发安全 O(1) Map.set O(N) spawn
适用场景 SDK 编程式注入 用户配置

六、AsyncHookRegistry:异步钩子注册表

来源:src/utils/hooks/AsyncHookRegistry.ts

异步协议

钩子可以在 stdout 输出 {"async": true, "asyncTimeout": 15000} 后继续后台运行(不阻塞主流程)。

钩子进程     stdout: {"async": true, "asyncTimeout": 15000}
↓ 立即返回
CC 主流程 继续下一步
↓ 稍后在 query loop 轮询
CC 查询 checkForAsyncHookResponses() 发现完成
↓ 注入响应

核心数据结构

type PendingAsyncHook = {
processId: string
hookId: string
timeout: number // 默认 15s
shellCommand?: ShellCommand
stopProgressInterval: () => void // 清理进度 interval
responseAttachmentSent: boolean // 防重复投递
}

const pendingHooks = new Map<string, PendingAsyncHook>() // 全局单例

轮询与清理

// 在 query loop 每轮调用
const responses = await checkForAsyncHookResponses()

// 处理完成的 hook
for (const r of responses) {
// 注入到 model messages
}
removeDeliveredAsyncHooks(processIds)

// Session 结束时
await finalizePendingAsyncHooks() // kill 未完成的,cleanup 所有

SessionStart 特殊处理:SessionStart 的异步钩子完成后,自动调用 invalidateSessionEnvCache() 使环境变量缓存失效,确保下一次工具调用使用最新环境。

进度事件:每 1000ms 轮询 shellCommand.taskOutput,有新 stdout 时通过 emitHookProgress 广播。


七、SSRF 防护(HTTP 钩子专用)

来源:src/utils/hooks/ssrfGuard.ts

阻断的 IP 范围

IPv4 阻断:
0.0.0.0/8 "this" network
10.0.0.0/8 私有网络
100.64.0.0/10 CGNAT/共享地址(阿里云 metadata: 100.100.100.200)
169.254.0.0/16 链路本地(云 metadata:169.254.169.254)
172.16.0.0/12 私有网络
192.168.0.0/16 私有网络

IPv4 放行:
127.0.0.0/8 回环(本地开发策略服务器是主要用例)

IPv6 阻断:fc00::/7(本地唯一)、fe80::/10(链路本地)、::(未指定)
IPv6 放行:::1(回环)

防 DNS 重绑定

ssrfGuardedLookup 作为 axios lookup 回调,在 DNS 解析完成后、socket 连接前验证 IP,消除 Time-of-Check-to-Time-of-Use 窗口。

代理绕过:当沙箱代理或环境变量代理生效时,跳过 SSRF guard(代理本身执行 DNS,guard 会错误地检查代理的 IP)。

IPv4-mapped IPv6 防绕过

::ffff:169.254.169.254  →  提取 169.254.169.254  →  阻断
::ffff:a9fe:a9fe → 展开 hex 组 → 提取 169.254.169.254 → 阻断

expandIPv6Groups() 先展开 :: 压缩形式,再提取嵌入的 IPv4 地址,避免 hex 形式绕过。


八、Hook 事件广播系统

来源:src/utils/hooks/hookEvents.ts

事件类型

type HookStartedEvent  = { type: 'started'; hookId; hookName; hookEvent }
type HookProgressEvent = { type: 'progress'; ...; stdout; stderr; output }
type HookResponseEvent = { type: 'response'; ...; exitCode?; outcome: 'success'|'error'|'cancelled' }

广播策略

模式 总是广播 可选广播(需开启)
默认 SessionStart、Setup 其余 25 个事件
SDK includeHookEvents: trueCLAUDE_CODE_REMOTE 全部 27 个

setAllHookEventsEnabled(true) 开启全量广播。

Pending Buffer

handler 注册前积压的事件存入 pendingEvents(上限 100 条,超出丢弃最旧)。handler 注册时立即 replay 所有积压事件。


九、Hooks 配置快照机制

来源:src/utils/hooks/hooksConfigSnapshot.ts

Hooks 配置在 session 初始化时快照一次(避免运行中配置热更新带来不确定性)。关键函数:

getHooksConfigFromSnapshot()     // 使用快照中的 hooks
shouldAllowManagedHooksOnly() // 企业策略:只允许 managed hooks
shouldDisableAllHooksIncludingManaged() // 完全禁用所有 hooks

ConfigChange 事件本身由文件监控触发,hook 可 exit 2 阻止新配置应用到当前会话。


十、面试必考点

Q1:Claude Code 的钩子系统如何工作?能实现哪些能力?

:Hooks 是用户/企业在 Agent 生命周期关键节点注入自定义逻辑的机制。27 个事件覆盖工具调用前后、会话开始/结束、用户输入提交、压缩、权限请求等全链路。

核心能力三类:

  • 观察:记录日志(exit 0,stdout 不打扰 model)
  • 阻断:exit 2 阻止危险操作(如 PreToolUse 阻止写生产数据库)
  • 增强:exit 0 stdout 向 Claude 注入上下文(如 SessionStart 注入团队规范)

Q2:exit code 协议是什么?

:不同事件的 exit code 语义不同,但通用模式:

  • exit 0:成功,stdout 可能显示给 Claude 或用户(事件决定)
  • exit 2阻断,stderr 发给 model 或阻止动作(事件决定)
  • 其他非零:向用户显示 stderr,主流程继续

PreToolUse exit 2 = 阻止工具执行 + 向 model 发错误;Stop exit 2 = 继续对话(Claude 再想想)。

Q3:5 种钩子类型各有什么适用场景?

类型 适用场景 性能开销
command 日志、通知、调用现有脚本 低(子进程)
prompt 简单 LLM 验证(30s 超时) 中(单次 API 调用)
agent 复杂验证,需要读文件/工具(50 轮) 高(多轮 query)
http 企业策略服务、外部审批 低(HTTP POST)
function SDK 编程式注入(不持久化) 极低(进程内)

Q4:SessionHooks 为什么用 Map 而不是 Record?

:高并发场景(parallel() 启动 N 个 agent 在同一 tick 调用 addFunctionHook):

  • Record + spread:O(N²) 复制成本 + 触发 ~30 个 store listeners
  • Map:O(1) set + return prev(引用不变)→ Object.is 检查短路,零 listener 触发

Q5:HTTP 钩子的安全机制是什么?

:三层防护:

  1. URL 白名单allowedHttpHookUrls(企业管理员设置,* 通配符)
  2. SSRF 防护ssrfGuardedLookup 在 DNS 解析完成后检查 IP,阻断私有/链路本地地址;特别处理 IPv4-mapped IPv6 防绕过
  3. env 变量白名单:header 中的 $VAR 插值只展开 allowedEnvVars 中的变量,防止 secrets 泄露;结果 sanitize 去除 \r\n\x00 防 CRLF 注入

Q6:异步钩子如何不阻塞主流程?

:钩子进程在 stdout 输出 {"async": true} 后即返回,主流程继续。AsyncHookRegistry 保存对进程的引用,query loop 在每轮 checkForAsyncHookResponses() 轮询是否完成,完成后注入响应。SessionStart 的异步钩子完成后额外触发 invalidateSessionEnvCache()

Q7:企业如何锁定钩子?

allowManagedHooksOnly = true → userSettings/projectSettings/localSettings 的钩子全部忽略,只有管理员在 policySettings 中定义的 hooks 生效。shouldDisableAllHooksIncludingManaged = true → 完全禁用钩子,包括 managed hooks。

涉及源文件

  • src/utils/hooks.ts
  • src/utils/hooks/
打赏
  • 微信
  • 支付宝

评论