23-hooks-ji-zhi
23-hooks-ji-zhi

23-hooks-ji-zhi

Claude Code 的 Hooks 机制是一种强大的生命周期拦截系统,允许开发者在 Agent 执行过程中的关键节点注入自定义逻辑。通过 Hooks,你可以实现安全检查、上下文注入、工具调用拦截、自动化验证等多种功能。

架构概览

Hooks 系统的核心架构包含三个主要层次:事件定义层、匹配执行层和输出处理层。

flowchart TB
    subgraph 事件层
        HE[Hook 事件<br/>25 种事件类型]
    end

    subgraph 配置层
        SC[Settings 配置]
        RC[Registered Hooks<br/>SDK 注册]
        SH[Session Hooks<br/>运行时注册]
        FH[Function Hooks<br/>函数回调]
    end

    subgraph 执行层
        GM[getMatchingHooks<br/>匹配引擎]
        EH[execCommandHook<br/>命令执行]
        PH[execPromptHook<br/>提示执行]
        AH[execAgentHook<br/>Agent 执行]
        HH[execHttpHook<br/>HTTP 执行]
    end

    subgraph 输出层
        PO[processHookJSONOutput<br/>JSON 处理]
        AR[AggregatedHookResult<br/>结果聚合]
        PR[AsyncHookRegistry<br/>异步注册]
    end

    HE --> GM
    SC --> GM
    RC --> GM
    SH --> GM
    FH --> GM
    GM --> EH
    GM --> PH
    GM --> AH
    GM --> HH
    EH --> PO
    PH --> PO
    AH --> PO
    HH --> PO
    PO --> AR
    AR --> PR

25 种 Hook 事件

Claude Code 定义了 25 种 Hook 事件,覆盖完整的 Agent 生命周期(定义于 src/entrypoints/sdk/coreTypes.ts:25-53):

阶段 事件 触发时机 匹配字段
会话 SessionStart 会话启动 source
SessionEnd 会话结束 reason
Setup 初始化完成 trigger
用户交互 UserPromptSubmit 用户提交消息 —
Stop Agent 停止响应 —
StopFailure Agent 停止失败 error
工具执行 PreToolUse 工具调用前 tool_name
PostToolUse 工具调用后(成功) tool_name
PostToolUseFailure 工具调用后(失败) tool_name
权限 PermissionRequest 权限请求 tool_name
PermissionDenied 权限被拒 tool_name
子 Agent SubagentStart 子 Agent 启动 agent_type
SubagentStop 子 Agent 停止 agent_type
压缩 PreCompact 上下文压缩前 trigger
PostCompact 上下文压缩后 trigger
协作 TeammateIdle Teammate 空闲 —
TaskCreated 任务创建 —
TaskCompleted 任务完成 —
MCP Elicitation MCP 服务器请求用户输入 mcp_server_name
ElicitationResult Elicitation 结果返回 mcp_server_name
环境 ConfigChange 配置变更 source
CwdChanged 工作目录变更 —
FileChanged 文件变更 file_path
InstructionsLoaded 指令加载 load_reason
WorktreeCreate Worktree 创建 —
WorktreeRemove Worktree 删除 —

Sources: coreTypes.ts

六种 Hook 类型

Hooks 配置支持六种执行方式(定义于 src/schemas/hooks.ts):

类型 执行方式 适用场景
command Shell 命令(bash/PowerShell) 通用脚本、CI 检查
prompt 启动 LLM 评估 复杂语义分析
agent 启动子 Agent 执行 复杂多步骤验证
http HTTP POST 请求 远程服务、Webhook
callback 内部 JS 函数 系统内置 Hook
function 运行时注册的函数 Hook Agent/Skill 内部使用

Command Hook 配置

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "check-security.sh ${CLAUDE_PROJECT_DIR}",
        "shell": "bash",
        "timeout": 30,
        "if": "Bash(git push*)",
        "async": false,
        "asyncRewake": false
      }]
    }]
  }
}

Sources: hooks.ts

核心执行引擎

命令型 Hook 执行

execCommandHook()(src/utils/hooks.ts:829-1417)是命令型 Hook 的执行核心,其执行流程如下:

sequenceDiagram
    participant Caller
    participant execCommandHook
    participant Shell
    participant Sandbox
    participant AsyncRegistry

    Caller->>execCommandHook: 执行 hook
    execCommandHook->>execCommandHook: 解析 shell 类型
    Note over execCommandHook: bash: Git Bash on Windows<br/>powershell: pwsh

    execCommandHook->>execCommandHook: 变量替换
    Note over execCommandHook: ${CLAUDE_PLUGIN_ROOT}<br/>${CLAUDE_PLUGIN_DATA}<br/>${user_config.X}

    execCommandHook->>Sandbox: 应用网络沙箱
    Note over Sandbox: 命令型 Hook 仅网络限制<br/>文件系统自由访问

    alt asyncRewake 模式
        execCommandHook->>execCommandHook: spawn 并注册退出处理
        execCommandHook-->>Caller: 立即返回
    else 标准执行
        execCommandHook->>Shell: spawn 子进程
        Shell-->>execCommandHook: stdout/stderr 流
        loop 直到完成/超时/中止
            execCommandHook->>execCommandHook: 检查第一行是否为 {"async":true}
        end

        alt 检测到异步标记
            execCommandHook->>AsyncRegistry: 注册后台 Hook
            execCommandHook-->>Caller: backgrounded: true
        else 同步执行完成
            execCommandHook-->>Caller: 返回 stdout/stderr
        end
    end

Sources: hooks.ts

变量替换机制

Hook 命令支持多种变量替换(src/utils/hooks.ts:880-950):

变量 说明 适用事件
${CLAUDE_PLUGIN_ROOT} 插件根目录 所有事件
${CLAUDE_PLUGIN_DATA} 插件数据目录 所有事件
${user_config.X} 用户配置值 所有事件
$CLAUDE_PROJECT_DIR 项目根目录(环境变量) 所有事件
$CLAUDE_ENV_FILE 环境变量文件 SessionStart/Setup/CwdChanged/FileChanged

环境变量注入

const envVars: NodeJS.ProcessEnv = {
  ...subprocessEnv(),
  CLAUDE_PROJECT_DIR: toHookPath(projectDir),
}

// 插件选项暴露为环境变量
if (pluginOpts) {
  for (const [key, value] of Object.entries(pluginOpts)) {
    const envKey = key.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()
    envVars[`CLAUDE_PLUGIN_OPTION_${envKey}`] = String(value)
  }
}

Sources: hooks.ts

异步 Hook 协议

async 模式

当 Hook 进程的 stdout 第一行是 {"async":true} 时,系统将其转为后台任务(src/utils/hooks.ts:1199-1246):

const firstLine = firstLineOf(stdout).trim()
if (isAsyncHookJSONOutput(parsed)) {
  executeInBackground({
    processId: `async_hook_${child.pid}`,
    asyncResponse: parsed,
    ...
  })
}

后台 Hook 通过 registerPendingAsyncHook() 注册到 AsyncHookRegistry,完成后通过 enqueuePendingNotification() 通知主线程。

Sources: AsyncHookRegistry.ts

asyncRewake 模式

asyncRewake 模式的 Hook 绕过 AsyncHookRegistry。当 Hook 退出码为 2 时,通过 enqueuePendingNotification() 以 task-notification 模式注入消息,唤醒空闲的模型:

if (result.code === 2) {
  enqueuePendingNotification({
    value: wrapInSystemReminder(
      `Stop hook blocking error from command "${hookName}": ${stderr || stdout}`,
    ),
    mode: 'task-notification',
  })
}

Sources: hooks.ts

Hook 输出 Schema

同步输出结构

同步 Hook 的输出遵循严格的 Zod schema(src/types/hooks.ts:49-180):

{
  "continue": false,
  "suppressOutput": true,
  "stopReason": "安全检查失败",
  "decision": "approve" | "block",
  "reason": "原因说明",
  "systemMessage": "警告内容",
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow" | "deny" | "ask",
    "permissionDecisionReason": "匹配了安全规则",
    "updatedInput": { ... },
    "additionalContext": "额外上下文"
  }
}

Sources: hooks.ts

事件特定输出

事件 专有字段 作用
PreToolUse permissionDecision, updatedInput, additionalContext 拦截/修改工具输入
UserPromptSubmit additionalContext 注入额外上下文
PostToolUse additionalContext, updatedMCPToolOutput 修改 MCP 工具输出
SessionStart initialUserMessage, watchPaths 设置初始消息和文件监控
PermissionDenied retry 指示是否重试
Elicitation action, content 控制用户输入对话框

Sources: hooks.ts

匹配机制

多来源合并

getHooksConfig()(src/utils/hooks.ts:1600-1720)从多个来源收集 Hook 配置:

flowchart LR
    subgraph getHooksConfig
        HSC[Settings Snapshot<br/>user/project/local]
        RH[Registered Hooks<br/>SDK 回调]
        SH[Session Hooks<br/>运行时注册]
        FH[Function Hooks<br/>内存函数]
    end

    HSC --> |合并| MC[HookMatchers]
    RH --> |合并| MC
    SH --> |合并| MC
    FH --> |合并| MC

Sources: hooks.ts

匹配规则

matcher 字段支持四种模式(src/utils/hooks.ts:1420-1460):

模式 示例 说明
精确匹配 "Write" 完全匹配工具名
多值匹配 "Write\|Edit" 管道分隔的多个值
正则匹配 "^Bash(git.*)" 正则表达式
通配匹配 "*" 或 "" 匹配所有
function matchesPattern(matchQuery: string, matcher: string): boolean {
  if (!matcher || matcher === '*') return true
  if (matcher.includes('|')) {
    return matcher.split('|').some(m => matchesPattern(matchQuery, m.trim()))
  }
  if (matcher.startsWith('^')) {
    const regex = new RegExp(matcher)
    return regex.test(matchQuery)
  }
  return matcher === matchQuery
}

Sources: hooks.ts

if 条件过滤

Hook 可以指定 if 条件,使用权限规则语法进行精细过滤(src/utils/hooks.ts:1472-1520):

{
  "hooks": [{
    "matcher": "Bash",
    "hooks": [{
      "type": "command",
      "command": "check-git-branch.sh",
      "if": "Bash(git push*)"
    }]
  }]
}

if 条件通过 prepareIfConditionMatcher() 预编译匹配器,支持 Bash 工具的 AST 级别命令解析。

Sources: hooks.ts

Hook 去重

同一 Hook 命令在不同配置层级可能重复。系统按 pluginRoot\0command 做 Map 去重,保留最后合并的层级:

function hookDedupKey(m: MatchedHook, payload: string): string {
  return `${m.pluginRoot ?? m.skillRoot ?? ''}\0${payload}`
}

Sources: hooks.ts

安全机制

工作区信任检查

所有 Hook 都要求工作区信任(src/utils/hooks.ts:286-310)。这是纵深防御措施——防止恶意仓库的 .claude/settings.json 在未信任的情况下执行任意命令:

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

  const hasTrust = checkHasTrustDialogAccepted()
  return !hasTrust
}

Sources: hooks.ts

网络沙箱

命令型 Hook 启用时,系统对其应用网络限制(src/utils/hooks.ts:1020-1060):

if (SandboxManager.isSandboxingEnabled()) {
  sandboxedCommand = await SandboxManager.wrapWithSandbox(
    finalCommand,
    undefined,
    {
      network: {
        allowedDomains: [],  // 拒绝所有出站
        deniedDomains: [],
      },
      filesystem: {
        allowWrite: ['/'],  // 允许读写项目文件
        denyWrite: [],
        allowRead: [],
        denyRead: [],
      },
    },
    signal,
  )
}

Sources: hooks.ts

HTTP Hook 策略

HTTP Hook 支持 URL 白名单和请求头环境变量插值(src/utils/hooks/execHttpHook.ts):

function getHttpHookPolicy() {
  const settings = settingsModule.getInitialSettings()
  return {
    allowedUrls: settings.allowedHttpHookUrls,
    allowedEnvVars: settings.httpHookAllowedEnvVars,
  }
}

Sources: execHttpHook.ts

生命周期钩子实战

PreToolUse:拦截工具调用

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "security-check.sh",
        "if": "Bash(curl|wget|nc|ssh)"
      }]
    }]
  }
}

返回 permissionDecision: "deny" 将阻止工具执行并显示阻塞错误。

SessionStart:初始化上下文

{
  "hooks": {
    "SessionStart": [{
      "matcher": "",
      "hooks": [{
        "type": "prompt",
        "prompt": "分析项目结构,返回关键文件列表:\n$ARGUMENTS",
        "model": "claude-sonnet-4-6"
      }]
    }]
  }
}

返回 initialUserMessage 可设置会话的初始用户消息。

PostToolUse:修改 MCP 工具输出

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Read",
      "hooks": [{
        "type": "agent",
        "prompt": "验证读取的文件不包含敏感信息:\n$ARGUMENTS"
      }]
    }]
  }
}

返回 updatedMCPToolOutput 可替换 MCP 工具的原始输出。

Sources: hooks.ts

性能优化

快速路径

当所有 Hook 都是内部回调时,系统使用快速路径(src/utils/hooks.ts:2190-2220):

if (matchedHooks.every(
  m => m.hook.type === 'callback' || m.hook.type === 'function',
)) {
  // 跳过 span/progress/resultLoop
  // 测量:6.01µs → ~1.8µs (-70%)
  for (const { hook } of matchedHooks.entries()) {
    if (hook.type === 'callback') {
      await hook.callback(hookInput, toolUseID, signal, i, context)
    }
  }
  return
}

Sources: hooks.ts

事件过滤

hasHookForEvent() 函数用于快速检查是否存在匹配的 Hook,避免不必要的初始化开销:

function hasHookForEvent(hookEvent, appState, sessionId): boolean {
  const snap = getHooksConfigFromSnapshot()?.[hookEvent]
  if (snap?.length > 0) return true
  const reg = getRegisteredHooks()?.[hookEvent]
  if (reg?.length > 0) return true
  if (appState?.sessionHooks.get(sessionId)?.hooks[hookEvent]) return true
  return false
}

Sources: hooks.ts

下一步

  • 了解如何通过 Skills 开发 创建可复用的 Hook 打包
  • 探索 MCP 协议集成 与 Hooks 的结合使用
  • 参考 权限模型 理解 Hooks 在安全体系中的位置

相关条目

  • 22-skills-ji-neng-kai-fa
  • 20-memory-ji-yi-xi-tong

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注