跳转至

Deep Dive | utils/hooks.ts 5022 行 Hook 系统拆解

重要性:⭐⭐⭐⭐(用户/项目级扩展的关键机制) 真实位置src/utils/hooks.ts5022 行,项目第三大文件) 角色:让用户通过 ~/.claude/settings.json 配置钩子在 Claude Code 生命周期各阶段注入自定义行为 关联phase-04-components.md § 4.4topics/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 拒绝某个操作,必须返回这种结构。

流程

工具调用
PreToolUse hook 返回 HookBlockingError
工具被拒绝,注入 denial ToolResult
LLM 下一轮看到拒绝

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 关键设计

  1. 环境变量传递 input —— hook 读 CLAUDE_HOOK_INPUT 拿数据
  2. 10 分钟超时 —— 默认 10min
  3. AbortController —— 用户取消时 kill 子进程
  4. stdout 是 hook 输出 —— stderr 不算
  5. 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"

{ "continue": true, "decision": "approve", "reason": "OK" }

结构化输出 —— 不是 free text,可程序化处理
和 Webhook 同种思想

16.4 "4 种 hook 类型"

  • command —— 跑 shell 命令
  • http —— 调 HTTP endpoint
  • function —— 调 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 阻塞"是"拒绝"语义

// hook 返回 HookBlockingError
{ reason: "Tool not allowed" }

"阻塞" = 拒绝工具调用
和 "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. 阅读清单

  1. ✅ 完整通读 src/utils/hooks.ts(5022 行)
  2. ✅ 读 phase-04-components.md 配合
  3. 📌 读 src/hooks/ 目录的 85 个自定义 React hooks(区分清楚)
  4. 📌 读 src/types/message.ts 的 hook 相关字段
  5. 📌 读 src/utils/settings/settings.ts 了解配置解析

19. 练习任务

  1. 数 20+ executeXxxHooks 函数 —— 应该 20+ 个
  2. 设计你自己的"事件 hook 系统" —— 简化版 50 行
  3. 列出 Claude Code 实际能用 hook 做的 5 件事(写 5 个 settings.json 示例)
  4. 思考:为什么 hook 用 stdout JSON 输出而不是其他 IPC 方式?trade-off 是什么?