Published on

Claude Code Hook 系统深度解析:从源码看 AI 编程助手的可编程生命周期

Authors

引言

如果你用过 Claude Code,大概知道它支持在关键节点插入自定义脚本——比如在每次文件写入前跑 lint,在会话开始时自动加载环境变量,或者在 AI 即将输出时做最后一道检查。这些能力都由 Hook 系统 驱动。

但如果你看过 Claude Code 的源码,会发现这个 Hook 系统远比文档描述的要复杂和精妙。它不仅仅是"在某个时机跑个脚本"那么简单,而是一套完整的可编程生命周期框架,支持六种 Hook 类型、异步执行、LLM 驱动的智能 Hook、企业级权限控制,以及丰富的安全防护机制。

本文基于 Claude Code 最新源码,从架构设计到实现细节,全面拆解这套 Hook 系统。

一、Hook 的定义与六种类型

Claude Code 的 Hook 系统定义了六种执行方式(HookCommand 类型),每种适用于不同的场景:

1. Command Hook(命令型)

最基础的 Hook 类型,通过 bash 或 PowerShell 执行外部脚本。输入通过 stdin 以 JSON 传入,输出通过 stdout 返回。

// 配置示例
{
  "type": "command",
  "command": "npx eslint $ARGUMENTS",
  "shell": "bash",        // 可选,默认 bash
  "timeout": 30,          // 可选,超时秒数
  "if": "Bash(git *)"     // 可选,条件匹配
}

关键实现细节:

  • Shell 选择:hook.shell → DEFAULT_HOOK_SHELL,支持 bash 和 powershell 两种 shell
  • Windows 路径转换:bash hook 自动将 Windows 路径转为 POSIX 路径(C:\Users/c/Users),PowerShell 则跳过转换
  • 环境变量注入:CLAUDE_PROJECT_DIRCLAUDE_PLUGIN_ROOTCLAUDE_ENV_FILE
  • .sh 脚本自动处理:Windows 上自动为 .sh 文件添加 bash 前缀

2. Prompt Hook(提示型)

用 LLM(默认 Haiku)执行单轮验证。适合需要语义理解的场景,比如判断代码是否符合规范。

{
  "type": "prompt",
  "prompt": "检查 $ARGUMENTS 中的代码是否遵循项目规范。返回 {\"ok\": true/false, \"reason\": \"...\"}",
  "model": "claude-haiku"  // 可选,默认小快速模型
}

实现上,execPromptHook 会将 prompt 中的 $ARGUMENTS 替换为 JSON 输入,然后用 queryModelWithoutStreaming 进行单次 LLM 调用,要求返回结构化的 {ok, reason} 响应。

3. Agent Hook(代理型)

最强大的 Hook 类型,启动一个完整的 LLM Agent(最多 50 轮对话)来执行验证。Agent 可以使用所有可用工具来检查代码库。

{
  "type": "agent",
  "prompt": "验证以下修改是否完整实现了需求:$ARGUMENTS。使用工具检查代码库。"
}

execAgentHook 的实现非常精妙:

  • 创建独立的 hookAgentId,确保 Agent 的 session hooks 不会泄漏到主 Agent
  • 过滤掉 ALL_AGENT_DISALLOWED_TOOLS(防止 Agent 嵌套 Agent)
  • 通过 SyntheticOutputTool 强制结构化输出
  • 注册 Stop 事件上的 session hook 来强制 Agent 必须调用结构化输出工具
  • 最多 50 轮对话,超时后 abort

4. HTTP Hook(网络型)

向外部服务发送 HTTP POST 请求,适合与企业系统集成。

{
  "type": "http",
  "url": "https://api.example.com/hook",
  "headers": { "Authorization": "Bearer $MY_TOKEN" },
  "allowedEnvVars": ["MY_TOKEN"],
  "timeout": 60
}

安全特性:

  • URL 白名单(allowedHttpHookUrls
  • 环境变量插值白名单(防止通过 Hook 泄露密钥)
  • CRLF 注入防护(sanitizeHeaderValue
  • SSRF 防护(ssrfGuardedLookup,阻止私有 IP 访问)
  • 沙箱代理支持(sandboxing 启用时路由请求通过代理)

5. Callback Hook(回调型)

内部使用的 TypeScript 回调函数,由 SDK 或插件注册,不暴露给用户。

6. Function Hook(函数型)

会话级内存回调,通过 addFunctionHook 注册,无法持久化到 settings.json。用于如结构化输出强制等运行时验证。

addFunctionHook(
  setAppState,
  sessionId,
  'Stop',
  '',
  (messages, signal) => hasSuccessfulToolCall(messages, TOOL_NAME),
  'You MUST call the verification tool',
  { timeout: 5000 }
)

二、Hook 的注册与发现机制

Claude Code 的 Hook 来源有五个层级,按优先级从高到低:

1. 设置文件(Settings)

三个层级的 settings.json:

  • userSettings~/.claude/settings.json
  • projectSettings.claude/settings.json
  • localSettings.claude/settings.local.json

hooksSettings.tsgetAllHooks 中遍历所有来源,并做文件去重(避免在 home 目录下 userSettings 和 projectSettings 指向同一文件)。

2. 注册 Hook(Registered Hooks)

通过 getRegisteredHooks() 获取,包括:

  • SDK 回调(通过 registerHookCallbacks API)
  • 插件原生 Hook(PluginHookMatcher,包含 pluginRootpluginId

3. 会话 Hook(Session Hooks)

存储在 AppState.sessionHooks 中(一个 Map<string, SessionStore>),包括:

  • Skill frontmatter 注册的 Hook
  • Agent frontmatter 注册的 Hook
  • 运行时动态注册的 Function Hook

4. 快照机制

hooksConfigSnapshot.ts 实现了一个启动时快照模式:

  • captureHooksConfigSnapshot():启动时调用一次,冻结 Hook 配置
  • getHooksConfigFromSnapshot():所有执行路径都从快照读取
  • 尊重 allowManagedHooksOnlydisableAllHooks 策略

这意味着会话开始后修改 settings.json 不会影响当前会话的 Hook 配置

5. 去重策略

getMatchingHooks 中的去重逻辑非常细致:

  • 按 Hook 类型分别去重(command、prompt、agent、http)
  • 去重 key 包含 pluginRoot/skillRoot,避免不同插件的模板碰撞
  • if 条件不同的 Hook 视为不同 Hook
  • shell 类型是身份的一部分:{command:'echo x', shell:'bash'}{command:'echo x', shell:'powershell'} 是不同的 Hook

三、Hook 的执行生命周期

26 种事件类型

Claude Code 定义了令人惊叹的 26 种 Hook 事件(HOOK_EVENTS),覆盖了 AI 编程助手生命周期的方方面面:

工具生命周期:

  • PreToolUse:工具执行前(支持 permissionDecisionupdatedInput
  • PostToolUse:工具执行后(支持 additionalContextupdatedMCPToolOutput
  • PostToolUseFailure:工具执行失败后

权限系统:

  • PermissionDenied:自动模式拒绝工具调用时(支持 retry
  • PermissionRequest:权限对话框显示时(支持 decision: {behavior, updatedInput}

会话生命周期:

  • SessionStart:新会话开始(支持 initialUserMessagewatchPaths
  • SessionEnd:会话结束
  • Stop:AI 即将结束回复
  • StopFailure:API 错误导致回复结束
  • UserPromptSubmit:用户提交 prompt 时

Agent 系统:

  • SubagentStart / SubagentStop:子 Agent 开始/结束
  • TeammateIdle:队友即将空闲
  • TaskCreated / TaskCompleted:任务创建/完成

系统事件:

  • Notification:通知发送时
  • Setup:仓库初始化/维护
  • PreCompact / PostCompact:对话压缩前后
  • ConfigChange:配置文件变更
  • InstructionsLoaded:指令文件加载
  • WorktreeCreate / WorktreeRemove:工作树管理
  • CwdChanged / FileChanged:目录/文件变更
  • Elicitation / ElicitationResult:MCP 用户输入请求

执行流程

核心执行函数 executeHooks 的流程:

1. 检查 disableAllHooks / CLAUDE_CODE_SIMPLE
2. 检查 workspace trust(安全关键)
3. 获取匹配的 Hook(getMatchingHooks)
4. 分离 internal hooks(快速路径)vs user hooks
5. 并行执行所有 Hook(Promise.all)
6. 聚合结果:权限决策(deny > ask > allow 优先级)

对于纯内部回调 Hook(如 sessionFileAccessHooks),有一个极致优化:跳过 span/progress/abortSignal 等所有开销,直接批量执行回调。测量结果显示从 6.01µs 降到 ~1.8µs(-70%)。

异步 Hook 支持

Command Hook 支持异步模式,两种方式:

  1. 配置声明hook.async = true
  2. 协议检测:stdout 第一行输出 {"async": true, ...}

异步 Hook 通过 AsyncHookRegistry 管理:

  • 注册到 pendingHooks Map
  • 进度追踪(startHookProgressInterval,每秒轮询 stdout/stderr)
  • 完成时通过 checkForAsyncHookResponses 检查结果
  • asyncRewake 模式:Hook 完成后如果 exit code 2,通过 enqueuePendingNotification 唤醒 AI

四、退出码协议与输出解析

Command Hook 的退出码有一套精心设计的语义:

退出码含义影响
0成功stdout 可注入到 AI 上下文
2阻塞错误stderr 发送给 AI,阻止操作继续
其他非阻塞错误stderr 仅显示给用户

对于 JSON 输出,解析流程更复杂:

// 顶层字段
{
  "continue": false,           // 阻止继续
  "stopReason": "...",         // 停止原因
  "decision": "approve/block", // 权限决策
  "systemMessage": "...",      // 系统消息
  "suppressOutput": true,      // 抑制输出
  "reason": "...",             // 原因说明
  "hookSpecificOutput": {      // 事件特定输出
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow/deny/ask",
    "updatedInput": {...},     // 修改工具输入
    "additionalContext": "..."  // 额外上下文
  }
}

值得注意的是,hookSpecificOutput.hookEventName 必须与实际事件匹配,否则抛出错误。这是防止 Hook 输出被错误应用到错误事件的防护。

五、安全模型

Workspace Trust

所有 Hook 都要求 workspace trust。这是一个集中式的安全检查:

export function shouldSkipHookDueToTrust(): boolean {
  const isInteractive = !getIsNonInteractiveSession()
  if (!isInteractive) return false // SDK 模式隐式信任
  return !checkHasTrustDialogAccepted()
}

这个设计源于历史漏洞:SessionEnd Hook 在用户拒绝信任对话框时仍会执行。

企业策略控制

hooksConfigSnapshot.ts 实现了三层策略:

  1. disableAllHooks(managed):禁用所有 Hook,包括 managed
  2. allowManagedHooksOnly:仅允许 managed settings 中的 Hook
  3. disableAllHooks(non-managed):仅禁用非 managed Hook(managed Hook 仍然运行)

HTTP Hook 额外有:

  • allowedHttpHookUrls:URL 白名单
  • httpHookAllowedEnvVars:环境变量白名单

SSRF 防护

execHttpHook.ts 中的 ssrfGuardedLookup 阻止向私有 IP 发起请求(但允许 loopback 用于本地开发)。当使用代理(环境变量代理或沙箱代理)时自动跳过 SSRF 检查。

条件匹配安全

Hook 的 if 条件(如 "Bash(git *)")通过 prepareIfConditionMatcher 预编译,复用权限系统的 permissionRuleValueFromString 解析器和工具的 preparePermissionMatcher 方法,确保匹配逻辑与权限系统一致。

六、内置 Hook 示例

结构化输出强制

registerStructuredOutputEnforcement 是一个典型的内置 Function Hook:

export function registerStructuredOutputEnforcement(setAppState, sessionId) {
  addFunctionHook(
    setAppState,
    sessionId,
    'Stop',
    '',
    (messages) => hasSuccessfulToolCall(messages, SYNTHETIC_OUTPUT_TOOL_NAME),
    `You MUST call the ${SYNTHETIC_OUTPUT_TOOL_NAME} tool to complete this request.`,
    { timeout: 5000 }
  )
}

当 Agent Hook 的 Agent 即将结束回复但没有调用结构化输出工具时,这个 Hook 会阻止结束并提醒 Agent。

Skill/Agent Frontmatter Hook

registerSkillHooksregisterFrontmatterHooks 将 frontmatter 中定义的 Hook 注册为会话级 Hook:

# 在 skill/agent 的 frontmatter 中
hooks:
  Stop:
    - matcher: ''
      hooks:
        - type: command
          command: './verify.sh'
          once: true # 执行一次后自动移除

once: true 的 Hook 通过 onHookSuccess 回调在首次成功执行后自动移除。

文件变更监视

fileChangedWatcher.ts + CwdChanged / FileChanged Hook 构成了一个文件监视系统:

  • CwdChanged Hook 可以通过 hookSpecificOutput.watchPaths 注册监视路径
  • FileChanged Hook 在文件变更时触发,也可以动态更新监视列表
  • CLAUDE_ENV_FILE 环境变量允许 Hook 写入环境变量定义,后续 Bash 工具命令会自动加载

七、事件广播系统

hookEvents.ts 实现了一个独立于主消息流的 Hook 事件广播系统:

type HookExecutionEvent =
  | { type: 'started'; hookId; hookName; hookEvent }
  | { type: 'progress'; hookId; hookName; hookEvent; stdout; stderr; output }
  | { type: 'response'; hookId; hookName; hookEvent; output; stdout; stderr; exitCode; outcome }
  • SessionStartSetup 事件始终广播(低噪音)
  • 其他事件仅在 allHookEventsEnabled 时广播(SDK includeHookEvents 选项)
  • 支持事件缓冲(最多 100 个),handler 注册后立即投递积压事件

八、CLAUDE_CODE_SIMPLE 模式

通过环境变量 CLAUDE_CODE_SIMPLE=1 可以完全禁用所有 Hook 执行。这在 executeHooksexecuteHooksOutsideREPL 中都有检查。这个开关为简化模式(如 CI 环境)提供了快速关闭所有 Hook 扩展点的能力。

总结

Claude Code 的 Hook 系统是一个设计精良的可编程生命周期框架。它的核心设计哲学是:

  1. 安全优先:workspace trust、策略分层、SSRF 防护、环境变量白名单
  2. 渐进增强:从简单的 bash 脚本到完整的 LLM Agent,六种 Hook 类型覆盖不同复杂度需求
  3. 事件驱动:26 种精细事件让开发者可以在 AI 编程助手的任何关键节点插入自定义逻辑
  4. 企业友好:managed settings 策略、URL 白名单、配置变更审计
  5. 性能敏感:内部 Hook 快速路径、快照机制、条件预编译

这套系统让 Claude Code 不仅仅是一个 AI 编程助手,更像是一个可编程的 AI 操作系统内核——Hook 就是它的可加载内核模块。


本文基于 Claude Code 源码分析,涉及的源码文件主要包括:src/utils/hooks.ts(核心执行引擎)、src/utils/hooks/(各子模块)、src/utils/hooks/hooksConfigManager.ts(配置管理)、src/utils/hooks/hooksSettings.ts(设置读取)。