Skip to content
源码分析手册

Hook 系统架构:运行时生命周期的可编程扩展

Claude Code 的 Hook 系统是其高可扩展性的核心。它允许开发者在 CLI 运行时的关键节点(如会话启动、工具执行、权限请求、用户输入等)注入自定义逻辑,实现从简单的环境配置到复杂的企业级安全审计。

核心概念

Hook 系统本质上是 运行时状态机的同步/异步观测点(Observation Points)

不是一个简单的“插件系统”,因为它不提供改变核心逻辑的 API,而是通过监听事件并返回结构化指令来“引导”系统行为。它也不是后置的日志记录器,因为大多数 Hook 运行在关键动作执行之前,拥有阻断或修改后续流程的权力。

你可以把它看作是一个“带逻辑的防火墙”:当系统准备执行某个动作时,会先通过 Hook 询问外部脚本:“我要做这个了,你怎么看?”

源码级拆解

Hook 的核心驱动逻辑位于 claude-code-opensource/src/utils/hooks.ts。其实现流程遵循以下模式:

  1. 配置捕获与隔离: 在启动阶段,hooksConfigSnapshot.ts 会捕获当前的配置快照。Hook 可以定义在全局 ~/.claude/settings.json 或项目级 .claude/settings.json 中。这种快照机制确保了即使在会话中修改了配置文件,当前运行的 Hook 逻辑也是确定且隔离的。

  2. 按事件分派的调度函数: 源码中并不存在一个名为 executeHooks 的统一调度器。每类事件有独立的入口函数——executeSessionStartHooksexecutePreToolHooksexecutePostToolHooksexecutePermissionRequestHooks 等——但它们内部共享相同的底层执行模式:

    • 输入装配:通过 createBaseHookInput 构建结构化 JSON 输入(字段包括 session_idtranscript_pathcwdpermission_modeagent_id 等),经 stdin 传递给 Hook 进程。
    • 多模式派生:最终调用 execCommandHook(Shell 脚本)、execHttpHook(远程 API 调用)或 execAgentHook(由另一个 Claude 实例处理)。
    • 并发与超时:大多数 Hook 是并行执行的,各调度函数会严格管理超时,防止僵尸进程阻塞 CLI。
  3. 结构化指令协议(HookJSONOutput): Hook 通过 stdout 返回 JSON 格式的指令。系统会解析这些指令并将其转化为运行时的副作用,例如:

    • additionalContexts:向当前对话注入背景知识。
    • allow/deny:干预权限判定。
    • watchPaths:动态告诉系统需要监听哪些文件的变化。
  4. 条件过滤(Condition Matching):系统通过 prepareIfConditionMatcher 支持基于 if 语句的精细过滤。例如,你可以让某个 Hook 仅在调用 Bash 且参数包含 rm 时触发。

  5. 异步“非阻塞”执行:在 src/utils/hooks.ts 中,Hook 被分为同步阻塞与异步两种。若输出首行为 {"async": true} 或配置标记为 async: true,CLI 将通过 executeInBackground 将其转入后台运行。某些异步 Hook 甚至支持“重唤醒(Rewake)”机制,通过返回 Exit Code 2 来在后续轮次中重新激活模型。

  6. 超时与容错机制:系统内置多层超时保护。工具钩子(Tool Hooks)默认超时为 10 分钟,而会话结束钩子(SessionEnd)仅给予 1.5 秒窗口(SESSION_END_HOOK_TIMEOUT_MS_DEFAULT),防止清理脚本阻塞 CLI 退出。

实战注意事项

  • Bare 模式的物理隔绝:在 --bare 模式下,几乎所有 Hook 逻辑都会被物理跳过。
  • Managed 模式的约束:在受管环境下,仅允许受信任路径的回调,忽略自定义脚本。
  • 同步 Hook 的性能损耗:由于同步 Hook 会阻塞事件循环,建议将耗时任务改为异步模式。
  • 工作空间信任:Hook 运行前必须经过信任确认,未信任则静默跳过。
  • 副作用幂等性:某些操作(如 /compact)会重新触发 SessionStart,请保证操作幂等。

相关主题

源码锚点

  • claude-code-opensource/src/utils/hooks.ts — 各事件调度函数及底层执行器(execCommandHookexecHttpHookexecAgentHook)。
📄 src/utils/hooks.ts — 各事件调度函数及底层执行器(`execCommandHook`、`execHttpHook`、`execAgentHook`)。L747-748 of 5023
typescript
async function execCommandHook(
  hook: HookCommand & { type: 'command' },
  • claude-code-opensource/src/types/hooks.ts — 包含 SyncHookJSONOutputAsyncHookJSONOutput 的 Schema 定义。
📄 src/types/hooks.ts — 包含 `SyncHookJSONOutput` 与 `AsyncHookJSONOutput` 的 Schema 定义。L13-15 of 291
typescript
  SyncHookJSONOutput,
} from 'src/entrypoints/agentSdkTypes.js'
import type { Message } from 'src/types/message.js'
  • claude-code-opensource/src/utils/hooks/AsyncHookRegistry.ts — 异步钩子的后台托管、超时重试与重唤醒逻辑。
📄 src/utils/hooks/AsyncHookRegistry.ts — 异步钩子的后台托管、超时重试与重唤醒逻辑。L12-25 of 310
typescript
export type PendingAsyncHook = {
  processId: string
  hookId: string
  hookName: string
  hookEvent: HookEvent | 'StatusLine' | 'FileSuggestion'
  toolName?: string
  pluginId?: string
  startTime: number
  timeout: number
  command: string
  responseAttachmentSent: boolean
  shellCommand?: ShellCommand
  stopProgressInterval: () => void
}
  • claude-code-opensource/src/utils/hooks/hooksConfigSnapshot.ts — 钩子配置的加载与隔离策略。
📄 src/utils/hooks/hooksConfigSnapshot.ts — 钩子配置的加载与隔离策略。L8-37 of 134
typescript
let initialHooksConfig: HooksSettings | null = null

/**
 * Get hooks from allowed sources.
 * If allowManagedHooksOnly is set in policySettings, only managed hooks are returned.
 * If disableAllHooks is set in policySettings, no hooks are returned.
 * If disableAllHooks is set in non-managed settings, only managed hooks are returned
 * (non-managed settings cannot disable managed hooks).
 * Otherwise, returns merged hooks from all sources (backwards compatible).
 */
function getHooksFromAllowedSources(): HooksSettings {
  const policySettings = settingsModule.getSettingsForSource('policySettings')

  // If managed settings disables all hooks, return empty
  if (policySettings?.disableAllHooks === true) {
    return {}
  }

  // If allowManagedHooksOnly is set in managed settings, only use managed hooks
  if (policySettings?.allowManagedHooksOnly === true) {
    return policySettings.hooks ?? {}
  }

  // strictPluginOnlyCustomization: block user/project/local settings hooks.
  // Plugin hooks (registered channel, hooks.ts:1391) are NOT affected —
  // they're assembled separately and the managedOnly skip there is keyed
  // on shouldAllowManagedHooksOnly(), not on this policy. Agent frontmatter
  // hooks are gated at REGISTRATION (runAgent.ts:~535) by agent source —
  // plugin/built-in/policySettings agents register normally, user-sourced
  // agents skip registration under ["hooks"]. A blanket execution-time

基于 Claude Code v2.1.88 开源快照的深度分析