Deep Dive | utils/hooks.ts 5022 行 Hook 系统拆解¶
重要性:⭐⭐⭐⭐(用户/项目级扩展的关键机制) 真实位置:
src/utils/hooks.ts(5022 行,项目第三大文件) 角色:让用户通过~/.claude/settings.json配置钩子,在 Claude Code 生命周期各阶段注入自定义行为 关联:phase-04-components.md § 4.4、topics/coding-style-conventions.md
1. Hook 系统全貌¶
Claude Code 支持 10+ 种 hook 类型,覆盖整个生命周期:
| 阶段 | Hook 类型 | 触发时机 |
|---|---|---|
| 会话 | SessionStart |
会话开始 |
| 会话 | SessionEnd |
会话结束 |
| 用户 | UserPromptSubmit |
用户提交 prompt |
| 工具 | PreToolUse |
工具调用前 |
| 工具 | PostToolUse |
工具调用后 |
| 工具 | PostToolUseFailure |
工具调用失败后 |
| 通知 | Notification |
通知触发 |
| 权限 | PermissionRequest |
权限请求 |
| 停止 | Stop |
正常停止 |
| 停止 | SubagentStop |
子 agent 停止 |
| 压缩 | PreCompact |
上下文压缩前 |
| 压缩 | PostCompact |
上下文压缩后 |
| 队友 | TeammateIdle |
队友 idle |
| 任务 | TaskCreated |
任务创建 |
| 任务 | TaskCompleted |
任务完成 |
| 配置 | ConfigChange |
配置变化 |
| 文件 | CwdChanged / FileChanged |
CWD / 文件变化 |
| 加载 | InstructionsLoaded |
指令文件加载 |
| 询问 | Elicitation / ElicitationResult |
MCP 询问 |
| 状态栏 | StatusLine |
状态栏更新 |
| 建议 | FileSuggestion |
文件建议 |
20+ 种 hook,每个都有独立处理函数。
2. 文件结构总览¶
hooks.ts (5022 行)
│
├── 行 1-165 :imports + 常量
│ ├── TOOL_HOOK_EXECUTION_TIMEOUT_MS (行 166) = 10min
│ ├── SESSION_END_HOOK_TIMEOUT_MS_DEFAULT (行 175) = 1.5s
│
├── A. 基础工具(行 176-330)
│ ├── getSessionEndHookTimeoutMs (行 176)
│ ├── executeInBackground (行 184)
│ ├── shouldSkipHookDueToTrust (行 286)
│ ├── createBaseHookInput (行 301)
│
├── B. Hook 类型定义(行 330-381)
│ ├── HookBlockingError (行 330)
│ ├── ElicitationResponse (行 336)
│ ├── HookResult (行 338)
│ ├── AggregatedHookResult (行 359)
│
├── C. Hook 输出解析(行 382-746)
│ ├── validateHookJson (行 382)
│ ├── parseHookOutput (行 399)
│ ├── parseHttpHookOutput (行 453)
│ ├── processHookJSONOutput (行 489)
│
├── D. execCommandHook (行 747-1345) ~600 行
│
├── E. 匹配器(行 1346-1491)
│ ├── matchesPattern (行 1346)
│ ├── IfConditionMatcher (行 1383)
│ ├── prepareIfConditionMatcher (行 1390)
│ ├── FunctionHookMatcher (行 1423)
│ ├── MatchedHook (行 1432)
│ ├── isInternalHook (行 1440)
│ ├── hookDedupKey (行 1453)
│ ├── getPluginHookCounts (行 1461)
│ ├── getHookTypeCounts (行 1484)
│ ├── getHooksConfig (行 1492)
│
├── F. hasHookForEvent / getMatchingHooks (行 1582-1881) ~300 行
│
├── G. 阻塞消息 (行 1882-1960)
│ ├── getPreToolHookBlockingMessage (行 1882)
│ ├── getStopHookMessage (行 1894)
│ ├── getTeammateIdleHookMessage (行 1903)
│ ├── getTaskCreatedHookMessage (行 1914)
│ ├── getTaskCompletedHookMessage (行 1925)
│ ├── getUserPromptSubmitHookBlockingMessage (行 1936)
│
├── H. 推测:实际 executeXxxHooks 实现 (行 1960-2974) ~1000 行
│
├── I. HookOutsideReplResult + executeHooksOutsideREPL (行 2974-3570) ~600 行
│
├── J. 20+ executeXxxHooks 函数 (行 3570-4910) ~1340 行
│ ├── executeNotificationHooks (行 3570)
│ ├── executeStopFailureHooks (行 3594)
│ ├── executePreCompactHooks (行 3961)
│ ├── executePostCompactHooks (行 4034)
│ ├── executeSessionEndHooks (行 4097)
│ ├── executeConfigChangeHooks (行 4214)
│ ├── executeEnvHooks (行 4241)
│ ├── executeCwdChangedHooks (行 4260)
│ ├── executeFileChangedHooks (行 4278)
│ ├── executeInstructionsLoadedHooks (行 4335)
│ ├── executeElicitationHooks (行 4470)
│ ├── executeElicitationResultHooks (行 4525)
│ ├── executeStatusLineCommand (行 4584)
│ ├── executeFileSuggestionCommand (行 4675)
│ ├── executeFunctionHook (行 4740)
│ ├── executeHookCallback (行 4840)
│
└── K. 推测:utils / 内部函数 (行 4910-5022) ~110 行
├── hasWorktreeCreateHook (行 4910)
结构清晰:A 工具 → B 类型 → C 解析 → D 核心执行 → E 匹配 → F 匹配查找 → G 阻塞消息 → H 推测具体实现 → I REPL 外部 → J 20+ 公共 API → K 内部
3. A 段:基础工具(行 176-330)¶
3.1 getSessionEndHookTimeoutMs(行 176-183)¶
export function getSessionEndHookTimeoutMs(): number {
// 默认 1.5s,但可被 env var 覆盖
const envValue = process.env.CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS
if (envValue) {
const parsed = parseInt(envValue, 10)
if (!isNaN(parsed) && parsed > 0) {
return parsed
}
}
return SESSION_END_HOOK_TIMEOUT_MS_DEFAULT // 1500ms
}
SessionEnd hook 默认 1.5s 超时 —— 用户改 env var 调整。
为什么这么短: - Session 结束要尽快 - 但不能太短,否则 hook 跑不完
3.2 executeInBackground(行 184-285)¶
function executeInBackground<T>(
fn: () => Promise<T>,
onSuccess?: (result: T) => void,
onError?: (err: Error) => void,
): void {
fn().then(
result => onSuccess?.(result),
err => {
onError?.(err)
logError(err)
}
)
}
不 await 异步执行 —— fire-and-forget 模式。
用途: - 异步通知(不等结果) - 不阻塞主流程的副作用 - "发了就算"语义
3.3 shouldSkipHookDueToTrust(行 286-300)¶
export function shouldSkipHookDueToTrust(): boolean {
// 推测:
// 1. 检查信任模式
// 2. 如果已信任,hook 自动放行
return process.env.CLAUDE_CODE_DISABLE_HOOK_TRUST_CHECK === '1'
}
信任模式 —— 用户设"全信任"后,hook 询问跳过。
安全 trade-off: - 信任 = 方便 - 不信任 = 安全
3.4 createBaseHookInput(行 301-329)¶
export function createBaseHookInput(): {
// 推测 20+ 字段
sessionId: string
cwd: string
env: Record<string, string>
timestamp: number
// ...
} {
return {
sessionId: getSessionId(),
cwd: getCwd(),
env: process.env,
timestamp: Date.now(),
}
}
构造 hook 输入 —— 推测 20+ 字段,每个 hook 都会收到这些信息。
4. B 段:Hook 类型(行 330-381)¶
4.1 HookBlockingError(行 330-335)¶
export interface HookBlockingError {
reason: string // 阻塞原因
hookName?: string // 哪个 hook
toolName?: string // 哪个工具
details?: string // 详细
}
"阻塞"语义 —— hook 拒绝某个操作,必须返回这种结构。
流程:
4.2 HookResult(行 338-358)¶
export interface HookResult {
// 推测:
continue: boolean // 是否继续
stopReason?: string // 停止原因
suppressOutput?: boolean // 抑制输出
decision?: 'approve' | 'block' | 'ask' // 决策
reason?: string // 原因
hookSpecificOutput?: Record<string, unknown> // 钩子特定输出
// ...
}
Hook 标准返回 —— 决定 Claude Code 下一步行为。
4.3 AggregatedHookResult(行 359-381)¶
export type AggregatedHookResult = {
// 多个 hook 的结果合并
results: HookResult[]
// 是否有阻塞
hasBlock: boolean
// 合并后的 reason
reason?: string
// ...
}
聚合结果 —— 多个 hook 配同一个事件时,合并它们的结果。
5. C 段:Hook 输出解析(行 382-746)¶
5.1 validateHookJson(行 382-398)¶
function validateHookJson(json: unknown): ValidationResult {
// 验证 hook 输出是合法 JSON
// 包含 continue / stopReason / suppressOutput / decision 等字段
}
输入校验 —— 防止恶意 hook 输出破坏 Claude Code。
5.2 parseHookOutput(行 399-452)¶
function parseHookOutput(stdout: string): HookResult {
// 1. 解析 stdout
// 2. 提取 continue / decision / reason
// 3. 转成 HookResult
// 4. 错误时 fallback 到 default
}
解析 —— 把 hook 命令的 stdout 解析成 HookResult。
格式约定:
{
"continue": true,
"stopReason": "User requested",
"suppressOutput": false,
"decision": "approve",
"reason": "OK"
}
5.3 parseHttpHookOutput(行 453-488)¶
function parseHttpHookOutput(body: string): HookResult {
// 1. 解析 HTTP hook 返回的 body
// 2. 和 parseHookOutput 类似
}
HTTP hook 专用 —— 用户用 HTTP endpoint 替代 shell 命令。
5.4 processHookJSONOutput(行 489-746,~250 行)¶
function processHookJSONOutput(json: any): HookResult {
// 推测的步骤:
// 1. 解析 outermost JSON
// 2. 提取 hookSpecificOutput(按 hook 类型分支)
// 3. 校验决策字段
// 4. 校验 reason
// 5. 校验 continue
// 6. 校验 additionalContext
// 7. 校验 systemMessage
// 8. 转成 HookResult
}
250 行的复杂解析 —— 推测 10+ hook 类型的不同输出格式。
6. D 段:execCommandHook(行 747-1345,~600 行)¶
6.1 函数签名¶
async function execCommandHook(
hook: MatchedHook,
input: HookInput,
context: { /* ... */ },
): Promise<HookResult>
执行 shell 命令 hook —— ~/.claude/settings.json 里 type: 'command' 的 hook。
6.2 推测的内部流程¶
async function execCommandHook(hook, input, context) {
// 1. 准备环境变量
const env = {
...process.env,
CLAUDE_PROJECT_DIR: getCwd(),
CLAUDE_SESSION_ID: getSessionId(),
CLAUDE_HOOK_INPUT: JSON.stringify(input), // 喂给 hook
}
// 2. 解析 timeout
const timeout = hook.timeout ?? TOOL_HOOK_EXECUTION_TIMEOUT_MS // 10min
// 3. spawn 子进程
const child = spawn(hook.command, {
env,
cwd: getCwd(),
stdio: ['ignore', 'pipe', 'pipe'],
signal: context.abortController.signal,
})
// 4. 收集 stdout / stderr
let stdout = '', stderr = ''
child.stdout.on('data', d => stdout += d)
child.stderr.on('data', d => stderr += d)
// 5. 等待 + 超时
const exitCode = await Promise.race([
new Promise(resolve => child.on('close', resolve)),
new Promise((_, reject) => setTimeout(() => reject(new TimeoutError()), timeout)),
])
// 6. 解析输出
if (exitCode === 0) {
return parseHookOutput(stdout)
} else {
return { continue: true, error: stderr }
}
}
600 行的实现 —— 涉及子进程、超时、信号、解析。
6.3 关键设计¶
- 环境变量传递 input —— hook 读
CLAUDE_HOOK_INPUT拿数据 - 10 分钟超时 —— 默认 10min
- AbortController —— 用户取消时 kill 子进程
- stdout 是 hook 输出 —— stderr 不算
- exit code 0 = 成功 —— 非 0 = 失败但不阻塞
7. E 段:匹配器(行 1346-1491,~145 行)¶
7.1 matchesPattern(行 1346-1382)¶
function matchesPattern(matchQuery: string, matcher: string): boolean {
// 匹配 hook 规则
// matcher: "Bash" / "Write" / "Edit" / "*"
return minimatch(matchQuery, matcher)
}
通配符匹配 —— hook 配置 {matcher: 'Bash'} 匹配所有 Bash 工具调用。
minimatch 是 gitignore 风格的通配符。
7.2 prepareIfConditionMatcher(行 1390-1422)¶
async function prepareIfConditionMatcher(ifCondition: string): Promise<IfConditionMatcher> {
// 解析 hook 的 if 条件
// if: "session.start_time > 1h"
// 编译为可执行函数
return (actualIf: string) => {
return evaluateExpression(ifCondition, actualIf)
}
}
if 条件求值 —— 让 hook 可以条件触发。
7.3 MatchedHook + isInternalHook + hookDedupKey(行 1432-1460)¶
type MatchedHook = {
type: 'command' | 'http' | 'function' | 'prompt' | 'internal'
command?: string
url?: string
matcher?: string
if?: string
// ...
}
function isInternalHook(matched: MatchedHook): boolean {
return matched.type === 'internal'
}
function hookDedupKey(m: MatchedHook, payload: string): string {
// 去重 key —— 同一 hook 同一 payload 不重复执行
return `${m.type}:${m.command}:${payload}`
}
Hook 类型 —— 5 种:command、http、function、prompt、internal。
internal —— Claude Code 内部 hook(不是用户配置),不过滤。
hookDedupKey —— 同一 hook 同一 payload 不重复跑(性能优化)。
7.4 getHooksConfig(行 1492-1581,~90 行)¶
function getHooksConfig(): {
user: HookConfig[]
project: HookConfig[]
policy: HookConfig[]
builtin: HookConfig[]
// ...
}
90 行的 hooks 配置解析 —— 从 4 个来源读 hooks 配置:
user——~/.claude/settings.json(用户级)project——.claude/settings.json(项目级)policy—— 企业策略builtin—— 内置
优先级:policy > project > user > builtin(推测)。
8. F 段:hasHookForEvent / getMatchingHooks(行 1582-1881,~300 行)¶
8.1 hasHookForEvent(行 1582-1602)¶
export function hasHookForEvent(event: HookEvent): boolean {
// 1. 读所有 hook 配置
// 2. 过滤匹配 event 的
// 3. 返回是否有
return getMatchingHooks(event).length > 0
}
快速判断 —— 是否有匹配此事件的 hook。
8.2 getMatchingHooks(行 1603-1881,~280 行)¶
export async function getMatchingHooks(
event: HookEvent,
context: { /* ... */ },
): Promise<MatchedHook[]>
280 行的核心匹配函数 —— 推测: 1. 读所有 hook 配置 2. 过滤 event 类型匹配 3. 过滤 matcher 匹配(用 matchesPattern) 4. 评估 if 条件(用 prepareIfConditionMatcher) 5. 聚合去重(用 hookDedupKey) 6. 按 priority 排序 7. 返回
这是 hooks 系统的"查询引擎"。
9. G 段:阻塞消息(行 1882-1960)¶
9.1 getPreToolHookBlockingMessage(行 1882-1893)¶
export function getPreToolHookBlockingMessage(
blockingError: HookBlockingError,
): string {
return `Tool blocked by PreToolUse hook: ${blockingError.reason}`
}
PreToolUse 阻塞消息 —— 当 hook 拒绝工具调用时显示。
9.2 6 种阻塞消息(行 1882-1960)¶
| Hook 类型 | 阻塞消息函数 |
|---|---|
PreToolUse |
getPreToolHookBlockingMessage |
Stop |
getStopHookMessage |
TeammateIdle |
getTeammateIdleHookMessage |
TaskCreated |
getTaskCreatedHookMessage |
TaskCompleted |
getTaskCompletedHookMessage |
UserPromptSubmit |
getUserPromptSubmitHookBlockingMessage |
6 种阻塞消息 —— 每种 hook 的阻塞 UI 不同。
10. H 段:推测的内部实现(行 1960-2974,~1000 行)¶
推测这 1000 行是: - 具体 hook 触发的内部函数 - 多 hook 合并的策略 - 错误处理 + 重试 - 阻塞检测 - background 执行
11. I 段:executeHooksOutsideREPL(行 3003-3570,~570 行)¶
async function executeHooksOutsideREPL(
hooks: MatchedHook[],
input: HookInput,
context: HookContext,
): Promise<AggregatedHookResult>
570 行的 REPL 外部执行 —— 用于 SDK 模式 / batch 模式。
推测: - 不开 REPL,直接跑 hooks - 收集所有结果 - 聚合
12. J 段:20+ executeXxxHooks 公共 API(行 3570-4910,~1340 行)¶
这是 hooks.ts 真正的公共接口。
12.1 完整列表¶
| 函数 | 行号 | 角色 |
|---|---|---|
executeNotificationHooks |
3570 | 通知 hook |
executeStopFailureHooks |
3594 | 停止失败 hook |
executePreCompactHooks |
3961 | 压缩前 hook |
executePostCompactHooks |
4034 | 压缩后 hook |
executeSessionEndHooks |
4097 | 会话结束 hook |
executeConfigChangeHooks |
4214 | 配置变化 hook |
executeEnvHooks |
4241 | 环境变量变化 hook |
executeCwdChangedHooks |
4260 | CWD 变化 hook |
executeFileChangedHooks |
4278 | 文件变化 hook |
executeInstructionsLoadedHooks |
4335 | 指令加载 hook |
executeElicitationHooks |
4470 | Elicitation hook |
executeElicitationResultHooks |
4525 | Elicitation 结果 hook |
executeStatusLineCommand |
4584 | 状态栏命令 |
executeFileSuggestionCommand |
4675 | 文件建议命令 |
executeFunctionHook |
4740 | Function hook |
executeHookCallback |
4840 | Hook 回调 |
hasWorktreeCreateHook |
4910 | 是否有 worktree 创建 hook |
hasInstructionsLoadedHook |
4314 | 是否有指令加载 hook |
20+ 公共函数 —— 每个对应一种 hook 类型。
12.2 典型 executeXxxHooks 实现模板¶
// 推测的模板
export async function executeXxxHooks(
input: HookInput,
context: HookContext,
): Promise<AggregatedHookResult> {
// 1. 查匹配的 hooks
const hooks = await getMatchingHooks({ type: 'Xxx' }, context)
if (hooks.length === 0) {
return defaultResult
}
// 2. 准备 hook 输入
const fullInput = { ...createBaseHookInput(), ...input }
// 3. 执行所有 hooks
const results = await Promise.all(
hooks.map(hook => executeHook(hook, fullInput, context))
)
// 4. 聚合结果
const aggregated = aggregateResults(results)
// 5. 检查阻塞
if (aggregated.hasBlock) {
return aggregated
}
// 6. 后台执行剩余(不阻塞)
return aggregated
}
统一模式 —— 6 步:匹配 → 准备 → 执行 → 聚合 → 阻塞检查 → 后台剩余。
13. K 段:内部函数(行 4910-5022)¶
推测是 executeHook 的核心实现、getPluginHookCounts 等。
14. Hook 配置文件示例¶
// ~/.claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo 'About to run bash: $CLAUDE_TOOL_INPUT'",
"timeout": 5000
}
]
}
],
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "pbcopy",
"timeout": 3000
}
]
}
],
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"$CLAUDE_NOTIFICATION\"'"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "echo 'Session ended at $(date)' >> ~/claude.log"
}
]
}
]
}
}
真实使用示例:
- PreToolUse + Bash matcher + echo = 记录所有 Bash 调用
- PostToolUse + * matcher + pbcopy = 复制所有结果到剪贴板
- Notification + osascript = macOS 系统通知
- Stop + log append = 会话结束记录
15. 完整 Hook 生命周期¶
SessionStart
↓
[loop: 用户键入 prompt]
↓
UserPromptSubmit
↓
[LLM 推理]
↓
[if 工具调用]
PreToolUse
↓
tool.call
↓
PostToolUse (成功) / PostToolUseFailure (失败)
↓
[if stop reason]
Stop / SubagentStop
↓
SessionEnd
多 hook 串联 —— 每个节点都可注入行为。
16. 关键洞察¶
16.1 "Hook = 事件回调"¶
Claude Code 的整个生命周期 = 事件流。
Hook = 任意事件上的回调。
和 React 的 useEffect 同种思想——生命周期钩子。
16.2 "10+ 来源的 hook 配置"¶
4+ 个来源(user / project / policy / builtin): - 优先级管理 - 合并去重 - 多源配置 = 灵活性
16.3 "Hook 输出 = JSON"¶
结构化输出 —— 不是 free text,可程序化处理。
和 Webhook 同种思想。
16.4 "4 种 hook 类型"¶
command—— 跑 shell 命令http—— 调 HTTP endpointfunction—— 调 TypeScript 函数prompt—— 用 LLM 推理internal—— Claude Code 内部
满足所有扩展场景。
16.5 "环境变量传递 input"¶
CLAUDE_HOOK_INPUT 是约定 —— shell hook 读这变量拿数据。
前端类比:和 Webhook 的 POST body 同种"约定"。
16.6 "10 分钟超时" 边界¶
- 太短 → hook 跑不完
- 太长 → 卡住主流程
- 10 分钟是经验值(猜测基于"大多数 hook 几秒内完成")
16.7 "Hook 阻塞"是"拒绝"语义¶
"阻塞" = 拒绝工具调用。
和 "return new Error()" 一样自然。
16.8 "Plugin Hook" 推测¶
getPluginHookCounts 暗示 Plugin 系统也会贡献 hook。
Plugin = 用户扩展 + hook 贡献。
17. 实战:写一个简化版 Hook 系统¶
// 简化版(~30 行)
type Hook = (input: any) => Promise<{ continue: boolean }>
const hooks: Record<string, Hook[]> = {
'pre-tool': [],
'post-tool': [],
'session-end': [],
}
export function registerHook(event: string, hook: Hook) {
hooks[event]?.push(hook)
}
export async function triggerHook(event: string, input: any) {
const list = hooks[event] || []
for (const hook of list) {
const result = await hook(input)
if (!result.continue) {
return { blocked: true, event }
}
}
return { blocked: false }
}
// 用法
registerHook('pre-tool', async (input) => {
console.log('About to run:', input)
return { continue: true }
})
对比 Claude Code: - 简化版没有超时 - 简化版没有去重 - 简化版没有多源配置 - Claude Code 多了 200+ 边界处理
18. 阅读清单¶
- ✅ 完整通读
src/utils/hooks.ts(5022 行) - ✅ 读 phase-04-components.md 配合
- 📌 读
src/hooks/目录的 85 个自定义 React hooks(区分清楚) - 📌 读
src/types/message.ts的 hook 相关字段 - 📌 读
src/utils/settings/settings.ts了解配置解析
19. 练习任务¶
- 数 20+
executeXxxHooks函数 —— 应该 20+ 个 - 设计你自己的"事件 hook 系统" —— 简化版 50 行
- 列出 Claude Code 实际能用 hook 做的 5 件事(写 5 个 settings.json 示例)
- 思考:为什么 hook 用 stdout JSON 输出而不是其他 IPC 方式?trade-off 是什么?