Hook 系统架构:运行时生命周期的可编程扩展
Claude Code 的 Hook 系统是其高可扩展性的核心。它允许开发者在 CLI 运行时的关键节点(如会话启动、工具执行、权限请求、用户输入等)注入自定义逻辑,实现从简单的环境配置到复杂的企业级安全审计。
核心概念
Hook 系统本质上是 运行时状态机的同步/异步观测点(Observation Points)。
它不是一个简单的“插件系统”,因为它不提供改变核心逻辑的 API,而是通过监听事件并返回结构化指令来“引导”系统行为。它也不是后置的日志记录器,因为大多数 Hook 运行在关键动作执行之前,拥有阻断或修改后续流程的权力。
你可以把它看作是一个“带逻辑的防火墙”:当系统准备执行某个动作时,会先通过 Hook 询问外部脚本:“我要做这个了,你怎么看?”
源码级拆解
Hook 的核心驱动逻辑位于 claude-code-opensource/src/utils/hooks.ts。其实现流程遵循以下模式:
配置捕获与隔离: 在启动阶段,
hooksConfigSnapshot.ts会捕获当前的配置快照。Hook 可以定义在全局~/.claude/settings.json或项目级.claude/settings.json中。这种快照机制确保了即使在会话中修改了配置文件,当前运行的 Hook 逻辑也是确定且隔离的。按事件分派的调度函数: 源码中并不存在一个名为
executeHooks的统一调度器。每类事件有独立的入口函数——executeSessionStartHooks、executePreToolHooks、executePostToolHooks、executePermissionRequestHooks等——但它们内部共享相同的底层执行模式:- 输入装配:通过
createBaseHookInput构建结构化 JSON 输入(字段包括session_id、transcript_path、cwd、permission_mode、agent_id等),经 stdin 传递给 Hook 进程。 - 多模式派生:最终调用
execCommandHook(Shell 脚本)、execHttpHook(远程 API 调用)或execAgentHook(由另一个 Claude 实例处理)。 - 并发与超时:大多数 Hook 是并行执行的,各调度函数会严格管理超时,防止僵尸进程阻塞 CLI。
- 输入装配:通过
结构化指令协议(HookJSONOutput): Hook 通过
stdout返回 JSON 格式的指令。系统会解析这些指令并将其转化为运行时的副作用,例如:additionalContexts:向当前对话注入背景知识。allow/deny:干预权限判定。watchPaths:动态告诉系统需要监听哪些文件的变化。
条件过滤(Condition Matching):系统通过
prepareIfConditionMatcher支持基于if语句的精细过滤。例如,你可以让某个 Hook 仅在调用Bash且参数包含rm时触发。异步“非阻塞”执行:在
src/utils/hooks.ts中,Hook 被分为同步阻塞与异步两种。若输出首行为{"async": true}或配置标记为async: true,CLI 将通过executeInBackground将其转入后台运行。某些异步 Hook 甚至支持“重唤醒(Rewake)”机制,通过返回 Exit Code 2 来在后续轮次中重新激活模型。超时与容错机制:系统内置多层超时保护。工具钩子(Tool Hooks)默认超时为 10 分钟,而会话结束钩子(SessionEnd)仅给予 1.5 秒窗口(
SESSION_END_HOOK_TIMEOUT_MS_DEFAULT),防止清理脚本阻塞 CLI 退出。
实战注意事项
- Bare 模式的物理隔绝:在
--bare模式下,几乎所有 Hook 逻辑都会被物理跳过。 - Managed 模式的约束:在受管环境下,仅允许受信任路径的回调,忽略自定义脚本。
- 同步 Hook 的性能损耗:由于同步 Hook 会阻塞事件循环,建议将耗时任务改为异步模式。
- 工作空间信任:Hook 运行前必须经过信任确认,未信任则静默跳过。
- 副作用幂等性:某些操作(如
/compact)会重新触发SessionStart,请保证操作幂等。
相关主题
- 如果你想了解工具执行前后的精细控制,请看 PreToolUse:工具执行前的闸门。
- 如果你想深入了解异步 Hook 的后台注册机制,建议研究
AsyncHookRegistry.ts。 - 如果你想看会话层级的生命周期,请看 会话钩子:SessionStart 与 SessionStop。
源码锚点
claude-code-opensource/src/utils/hooks.ts— 各事件调度函数及底层执行器(execCommandHook、execHttpHook、execAgentHook)。
📄 src/utils/hooks.ts — 各事件调度函数及底层执行器(`execCommandHook`、`execHttpHook`、`execAgentHook`)。
async function execCommandHook(
hook: HookCommand & { type: 'command' },claude-code-opensource/src/types/hooks.ts— 包含SyncHookJSONOutput与AsyncHookJSONOutput的 Schema 定义。
📄 src/types/hooks.ts — 包含 `SyncHookJSONOutput` 与 `AsyncHookJSONOutput` 的 Schema 定义。
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 — 异步钩子的后台托管、超时重试与重唤醒逻辑。
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 — 钩子配置的加载与隔离策略。
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