CC源码剖析 #14 · Hook 系统:生命周期拦截点

基于 2026-03-31 公开的源码快照,~512,000 行代码


一、Hook 系统定位

Hook 系统是 Claude Code 的生命周期拦截点——允许外部代码(插件、本地脚本)在特定事件发生时介入并修改行为。

用户输入
    │
    ▼
┌─────────────────────────────────┐
│  Hook System                    │
│  processSessionStartHooks()      │
│  processSetupHooks()            │
└─────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────┐
│  QueryEngine                    │
│  preToolCall Hook              │
└─────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────┐
│  Tool.call()                    │
│  postToolCall Hook             │
└─────────────────────────────────┘
    │
    ▼
结果

二、Hook 类型定义

文件:src/types/hooks.ts

2.1 支持的事件类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// HOOK_EVENTS 列表
type HookEvent =
  | 'PreToolUse'          // 工具调用前
  | 'PostToolUse'         // 工具调用后
  | 'UserPromptSubmit'    // 用户提交输入前
  | 'UserPromptEdit'     // 用户编辑输入时
  | 'RoundStart'         // 回合开始
  | 'RoundEnd'           // 回合结束
  | 'SessionStart'       // 会话开始
  | 'SessionEnd'         // 会话结束
  | 'PreAgentCall'       // Agent 调用前
  | 'PostAgentCall'      // Agent 调用后
  | 'PreProcessModel'    // 模型处理前
  | 'PostProcessModel'   // 模型处理后

2.2 Hook 接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
interface HookDefinition {
  event: HookEvent
  name: string
  source: 'plugin' | 'builtin' | 'user'
  handler: HookHandler
  config?: HookConfig
}

type HookHandler = (
  context: HookContext,
  input: HookInput,
) => Promise<HookResult>

2.3 Hook 输入/输出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Hook 输入
interface HookInput {
  event: HookEvent
  sessionId: string
  messages: Message[]
  // 事件特定的输入数据
}

// Hook 输出
interface HookResult {
  continue: boolean            // 是否继续执行
  suppressOutput?: boolean     // 是否隐藏输出
  stopReason?: string         // 停止原因
  decision?: 'approve' | 'block'  // 决策
  reason?: string             // 决策原因
  systemMessage?: string       // 系统消息
  updatedInput?: object       // 修改后的输入
  hookSpecificOutput?: object  // Hook 特定输出
}

三、Hook 生命周期

3.1 会话启动 Hook

文件:src/utils/sessionStart.ts

1
2
3
4
5
6
7
async function processSessionStartHooks(
  hooks: HookDefinition[],
  context: HookContext,
): Promise<HookResult[]> {
  // 在会话开始时调用所有 SessionStart hooks
  // 按顺序执行,收集结果
}

3.2 会话结束 Hook

文件:src/utils/hooks.ts

1
2
3
4
5
6
7
8
async function executeSessionEndHooks(
  hooks: HookDefinition[],
  context: HookContext,
  timeoutMs: number = 5000,
): Promise<void> {
  // 会话结束时调用
  // 有超时保护,防止 hook 卡住会话关闭
}

3.3 Turn 级别 Hook

1
2
3
4
5
6
7
// 每个 turn 开始/结束时调用
// 注入时机:
// - preToolCall:工具调用前
// - postToolCall:工具调用后

onTurnStart(): void
onTurnEnd(): void

四、PreToolUse Hook

4.1 功能

在工具调用前拦截,可以:

  • 修改输入:更新工具参数
  • 阻止执行:返回 block 决定
  • 添加上下文:注入额外信息

4.2 配置示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// hooks.json
{
  "hooks": [
    {
      "event": "PreToolUse",
      "name": "validate-file-path",
      "handler": "./hooks/validate-path.ts",
      "config": {
        "allowedPaths": ["src/**"]
      }
    }
  ]
}

4.3 Handler 实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
async function validateFilePath(
  context: HookContext,
  input: { toolName: string; toolInput: object },
): Promise<HookResult> {
  if (input.toolName === 'FileEdit') {
    const filePath = input.toolInput.file_path
    if (!isAllowedPath(filePath, context.config.allowedPaths)) {
      return {
        decision: 'block',
        reason: `File path ${filePath} is not in allowed paths`,
      }
    }
  }
  return { continue: true }
}

五、UserPromptSubmit Hook

5.1 功能

在用户输入提交给 LLM 前拦截:

1
2
3
4
5
// 可以:
// - 清理输入(去除敏感信息)
// - 添加上下文
// - 验证输入
// - 拒绝输入

5.2 结果类型

1
2
3
4
5
6
7
8
// UserPromptSubmit Hook 特有输出
interface UserPromptSubmitResult extends HookResult {
  updatedInput?: {
    text?: string           // 修改后的文本
    attachments?: Attachment[]  // 添加附件
  }
  suppressOutput?: boolean
}

六、Hook 配置加载

6.1 配置文件位置

1
2
3
4
5
6
7
8
9
// 加载顺序(优先级从高到低):
// 1. .claude/hooks.json(项目本地)
// 2. ~/.claude/hooks.json(用户全局)
// 3. 企业托管 hooks(managed)

const HOOK_CONFIG_PATHS = [
  '.claude/hooks.json',
  '~/.claude/hooks.json',
]

6.2 Hook Schema 验证

文件:src/utils/settings/types.ts

1
2
3
4
5
6
7
8
const HooksSchema = z.object({
  hooks: z.array(z.object({
    event: z.enum(HOOK_EVENTS),
    name: z.string(),
    handler: z.string(),  // 文件路径或 inline code
    config: z.record(z.unknown()).optional(),
  }))
})

七、Plugin Hook 加载

文件:src/utils/plugins/loadPluginHooks.ts

7.1 插件 Hook 注册

1
2
3
4
5
6
// 插件可以提供 hook
// 通过 plugin.json 声明

interface PluginManifest {
  hooks: HookDefinition[]
}

7.2 加载流程

1
2
3
4
5
6
async function loadPluginHooks(): Promise<HookDefinition[]> {
  const plugins = await loadAllPluginsCacheOnly()
  return plugins
    .filter(p => p.manifest.hooks)
    .flatMap(p => p.manifest.hooks)
}

八、Hook 执行上下文

8.1 AppState

1
2
3
4
5
6
// Hook 可以访问和修改 AppState
interface HookContext {
  appState: AppState
  setAppState: (updater: (prev: AppState) => AppState) => void
  // ...
}

8.2 消息历史

1
2
3
4
5
6
// Hook 可以访问当前会话消息
interface HookContext {
  messages: Message[]
  appendMessage: (msg: Message) => void
  // ...
}

8.3 工具列表

1
2
3
4
5
// Hook 可以访问可用工具
interface HookContext {
  tools: Tool[]
  // ...
}

九、Hook 安全与隔离

9.1 权限控制

1
2
3
4
5
// Hook 执行在受限上下文
// 不能直接访问文件系统
// 需要通过 Tool 间接操作

// 危险操作需要用户确认

9.2 超时保护

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Hook 执行有超时限制
// 防止恶意或低效 Hook 卡住系统

const HOOK_TIMEOUT_MS = 5000

async function executeHookWithTimeout(
  hook: HookDefinition,
  context: HookContext,
): Promise<HookResult> {
  return Promise.race([
    hook.handler(context),
    timeout(HOOK_TIMEOUT_MS),
  ])
}

十、Hook 与 Skills 的区别

特性HookSkill
触发方式事件驱动显式调用
执行位置生命周期特定点任意位置
权限受限完整工具访问
配置声明式Markdown 文件
用途拦截/修改行为扩展功能

下一步

一篇:15 - 安全审查:Security Review 命令实现,深入安全审查命令的源码实现。


Claude Code 源码研究系列 · 2026 · skyseraph

SkySeraph
SkySeraph
AI for All & All for AI
留言 Comments