Skip to content

Latest commit

 

History

History
1120 lines (928 loc) · 33.8 KB

File metadata and controls

1120 lines (928 loc) · 33.8 KB

07 - Hooks 系统架构分析

基于 Claude Code v2.1.88 源码分析 源码路径: src/utils/hooks.ts, src/schemas/hooks.ts, src/types/hooks.ts, src/utils/hooks/


Architecture Diagram

Hook System Architecture

1. 概述

Hooks 系统是 Claude Code 的可扩展性核心,允许用户在 Claude Code 生命周期的关键节点注入自定义逻辑。 系统支持四种 Hook 类型(command / prompt / agent / http),覆盖 27 种事件, 并提供同步/异步两种执行模式、权限控制、模式匹配等能力。

主要源码文件:

文件 职责
src/schemas/hooks.ts Zod schema 定义(HookCommand, HookMatcher, HooksSettings)
src/types/hooks.ts TypeScript 类型定义(HookResult, HookJSONOutput 等)
src/utils/hooks.ts 核心执行引擎(~4600 行,所有 executeXxxHooks 函数)
src/utils/hooks/sessionHooks.ts Session 级别的运行时 Hook 管理
src/utils/hooks/hookEvents.ts Hook 事件广播系统
src/utils/hooks/hooksSettings.ts Hook 配置读取与 UI 展示
src/utils/hooks/hooksConfigSnapshot.ts 配置快照管理(启动时捕获)
src/utils/hooks/hookHelpers.ts Prompt/Agent hook 的辅助工具
src/utils/hooks/execPromptHook.ts Prompt hook 执行器
src/utils/hooks/execAgentHook.ts Agent hook 执行器
src/utils/hooks/execHttpHook.ts HTTP hook 执行器
src/utils/hooks/AsyncHookRegistry.ts 异步 Hook 注册表
src/hooks/toolPermission/ 权限审批交互(React hooks)

2. Hook 类型

2.1 类型定义 (Zod Schema)

所有 Hook 类型通过 z.discriminatedUnion('type', [...]) 定义在 src/schemas/hooks.ts:

// src/schemas/hooks.ts

// Command Hook -- 执行 shell 命令
const BashCommandHookSchema = z.object({
  type: z.literal('command'),
  command: z.string(),
  if: IfConditionSchema(),          // 可选: 权限规则语法过滤
  shell: z.enum(['bash', 'powershell']).optional(),
  timeout: z.number().positive().optional(),
  statusMessage: z.string().optional(),
  once: z.boolean().optional(),      // 一次性执行后移除
  async: z.boolean().optional(),     // 后台异步执行
  asyncRewake: z.boolean().optional(), // 异步执行,exit code 2 时唤醒模型
})

// Prompt Hook -- 调用 LLM 做单轮判断
const PromptHookSchema = z.object({
  type: z.literal('prompt'),
  prompt: z.string(),               // 支持 $ARGUMENTS 占位符
  if: IfConditionSchema(),
  timeout: z.number().positive().optional(),
  model: z.string().optional(),     // 默认使用 small fast model
  statusMessage: z.string().optional(),
  once: z.boolean().optional(),
})

// Agent Hook -- 启动多轮 LLM Agent 做验证
const AgentHookSchema = z.object({
  type: z.literal('agent'),
  prompt: z.string(),
  if: IfConditionSchema(),
  timeout: z.number().positive().optional(),
  model: z.string().optional(),
  statusMessage: z.string().optional(),
  once: z.boolean().optional(),
})

// HTTP Hook -- POST JSON 到远程 URL
const HttpHookSchema = z.object({
  type: z.literal('http'),
  url: z.string().url(),
  if: IfConditionSchema(),
  timeout: z.number().positive().optional(),
  headers: z.record(z.string(), z.string()).optional(),
  allowedEnvVars: z.array(z.string()).optional(),
  statusMessage: z.string().optional(),
  once: z.boolean().optional(),
})

// 联合类型
export const HookCommandSchema = z.discriminatedUnion('type', [
  BashCommandHookSchema, PromptHookSchema, AgentHookSchema, HttpHookSchema,
])

// 推导出的 TypeScript 类型
export type HookCommand = z.infer<typeof HookCommandSchema>
export type BashCommandHook = Extract<HookCommand, { type: 'command' }>
export type PromptHook = Extract<HookCommand, { type: 'prompt' }>
export type AgentHook = Extract<HookCommand, { type: 'agent' }>
export type HttpHook = Extract<HookCommand, { type: 'http' }>

2.2 内部 Hook 类型

除了用户可配置的四种类型,系统内部还有两种非持久化类型:

// src/types/hooks.ts -- Callback Hook (SDK 内部注册)
export type HookCallback = {
  type: 'callback'
  callback: (
    input: HookInput,
    toolUseID: string | null,
    abort: AbortSignal | undefined,
    hookIndex?: number,
    context?: HookCallbackContext,
  ) => Promise<HookJSONOutput>
  timeout?: number
  internal?: boolean  // 内部 hook 不计入遥测
}

// src/utils/hooks/sessionHooks.ts -- Function Hook (运行时回调)
export type FunctionHook = {
  type: 'function'
  id?: string
  timeout?: number
  callback: FunctionHookCallback  // (messages, signal?) => boolean
  errorMessage: string
  statusMessage?: string
}

Callback Hook: 由 SDK 或插件通过代码注册,如 attribution hooks、session file access hooks。 Function Hook: Session 级别的 TypeScript 回调,用于 Stop hook 中强制结构化输出等场景。


3. Hook 事件 (27 种)

所有事件定义在 src/entrypoints/sdk/coreTypes.ts:

export const HOOK_EVENTS = [
  'PreToolUse',          // 工具执行前
  'PostToolUse',         // 工具执行后
  'PostToolUseFailure',  // 工具执行失败后
  'Notification',        // 通知发送时
  'UserPromptSubmit',    // 用户提交 prompt 时
  'SessionStart',        // 新 session 启动
  'SessionEnd',          // session 结束
  'Stop',                // Claude 即将结束回复
  'StopFailure',         // API 错误导致回合结束
  'SubagentStart',       // 子 agent 启动
  'SubagentStop',        // 子 agent 结束
  'PreCompact',          // 上下文压缩前
  'PostCompact',         // 上下文压缩后
  'PermissionRequest',   // 权限对话框显示时
  'PermissionDenied',    // 自动模式拒绝工具调用后
  'Setup',               // 仓库初始化/维护
  'TeammateIdle',        // Teammate 即将空闲
  'TaskCreated',         // 任务创建时
  'TaskCompleted',       // 任务完成时
  'Elicitation',         // MCP 请求用户输入
  'ElicitationResult',   // 用户响应 MCP elicitation
  'ConfigChange',        // 配置文件变更
  'WorktreeCreate',      // 创建 worktree
  'WorktreeRemove',      // 移除 worktree
  'InstructionsLoaded',  // 指令文件加载时
  'CwdChanged',          // 工作目录切换后
  'FileChanged',         // 被监视文件变更
] as const

3.1 事件分类与退出码语义

分类 事件 Exit 0 Exit 2 其他 Exit
工具生命周期 PreToolUse stdout 不显示 stderr 发给模型,阻止工具调用 stderr 仅显示给用户
PostToolUse stdout 在 transcript 模式显示 stderr 立即发给模型 stderr 仅显示给用户
PostToolUseFailure stdout 在 transcript 模式显示 stderr 立即发给模型 stderr 仅显示给用户
用户交互 UserPromptSubmit stdout 发给 Claude 阻止处理,stderr 显示给用户 stderr 仅显示给用户
PermissionRequest 使用 hook 的决定 - stderr 仅显示给用户
PermissionDenied stdout 在 transcript 显示 - stderr 仅显示给用户
Session 生命周期 SessionStart stdout 发给 Claude 阻止错误被忽略 stderr 仅显示给用户
SessionEnd 命令成功完成 - stderr 仅显示给用户
Setup stdout 发给 Claude 阻止错误被忽略 stderr 仅显示给用户
停止控制 Stop stdout 不显示 stderr 发给模型,继续对话 stderr 仅显示给用户
StopFailure fire-and-forget - 输出被忽略
子 Agent SubagentStart stdout 发给子 agent 阻止错误被忽略 stderr 仅显示给用户
SubagentStop stdout 不显示 stderr 发给子 agent,继续运行 stderr 仅显示给用户
压缩 PreCompact stdout 追加为自定义压缩指令 阻止压缩 stderr 仅显示给用户
PostCompact stdout 显示给用户 - stderr 仅显示给用户
配置/文件 ConfigChange 允许变更 阻止变更应用 stderr 仅显示给用户
CwdChanged 命令成功完成 - stderr 仅显示给用户
FileChanged 命令成功完成 - stderr 仅显示给用户
InstructionsLoaded 纯观测性,不支持阻止 - stderr 仅显示给用户

4. HookMatcher -- 模式匹配

4.1 Schema 定义

// src/schemas/hooks.ts
export const HookMatcherSchema = z.object({
  matcher: z.string().optional(),  // 匹配模式
  hooks: z.array(HookCommandSchema()),
})

export const HooksSchema = z.partialRecord(
  z.enum(HOOK_EVENTS),
  z.array(HookMatcherSchema()),
)

4.2 匹配算法

matchesPattern() 函数 (src/utils/hooks.ts:1346) 实现三级匹配:

1. 空 matcher 或 "*"  -->  匹配所有
2. 纯字母数字 + "|"   -->  精确匹配(支持 pipe 分隔多值)
   例: "Write|Edit"  匹配 "Write" 或 "Edit"
3. 包含特殊字符       -->  作为正则表达式匹配
   例: "^Bash$"      匹配 "Bash"

4.3 匹配字段映射

不同事件使用不同字段作为 matchQuery:

// src/utils/hooks.ts:getMatchingHooks()
switch (hookInput.hook_event_name) {
  case 'PreToolUse':
  case 'PostToolUse':
  case 'PostToolUseFailure':
  case 'PermissionRequest':
  case 'PermissionDenied':
    matchQuery = hookInput.tool_name       // 匹配工具名
    break
  case 'SessionStart':
    matchQuery = hookInput.source          // 'startup' | 'resume' | 'clear' | 'compact'
    break
  case 'Setup':
    matchQuery = hookInput.trigger         // 'init' | 'maintenance'
    break
  case 'Notification':
    matchQuery = hookInput.notification_type
    break
  case 'FileChanged':
    matchQuery = basename(hookInput.file_path)
    break
  // ... 其他事件类似
}

4.4 if 条件过滤

Hook 支持 if 字段做更细粒度的过滤,使用权限规则语法:

{
  "type": "command",
  "command": "npm test",
  "if": "Bash(npm *)"
}

prepareIfConditionMatcher() 解析 if 条件,仅在 PreToolUse / PostToolUse / PostToolUseFailure / PermissionRequest 事件中生效。匹配逻辑复用了权限系统的 permissionRuleValueFromString 解析器和工具的 preparePermissionMatcher 方法。


5. Hook 执行流程

5.1 主执行引擎

executeXxxHooks()          各事件的入口函数
  |
  v
executeHooks()             核心执行引擎(async generator)
  |
  +-- shouldDisableAllHooksIncludingManaged()  全局禁用检查
  +-- shouldSkipHookDueToTrust()              工作区信任检查
  +-- getMatchingHooks()                       模式匹配 + 去重 + if 过滤
  |     +-- getHooksConfig()                   聚合所有来源的 hook 配置
  |     |     +-- getHooksConfigFromSnapshot()  settings.json 快照
  |     |     +-- getRegisteredHooks()          SDK/插件注册的 hooks
  |     |     +-- getSessionHooks()             Session 级动态 hooks
  |     |     +-- getSessionFunctionHooks()     Function hooks
  |     +-- matchesPattern()                   matcher 模式匹配
  |     +-- prepareIfConditionMatcher()        if 条件过滤
  |     +-- 去重(按 type + command/prompt/url 分组)
  |
  +-- 并行执行所有匹配的 hook
  |     +-- execCommandHook()  --> shell 命令执行
  |     +-- execPromptHook()   --> LLM 单轮查询
  |     +-- execAgentHook()    --> LLM 多轮 Agent
  |     +-- execHttpHook()     --> HTTP POST
  |     +-- callback()         --> 内部回调
  |     +-- FunctionHook       --> 运行时回调
  |
  +-- processHookJSONOutput()  解析 JSON 输出
  +-- yield AggregatedHookResult  逐个产出结果

5.2 Command Hook 执行 (execCommandHook)

Command Hook 是最复杂的执行路径:

1. Shell 选择: hook.shell --> 'bash'(默认) 或 'powershell'
2. 路径转换: Windows 上 bash 使用 POSIX 路径 (/c/Users/...)
3. 插件变量替换: ${CLAUDE_PLUGIN_ROOT}, ${user_config.X}
4. 环境变量注入:
   - CLAUDE_PROJECT_DIR: 项目根目录
   - CLAUDE_PLUGIN_ROOT: 插件根目录
   - CLAUDE_ENV_FILE: 环境脚本路径(SessionStart/Setup/CwdChanged/FileChanged)
5. 命令前缀: CLAUDE_CODE_SHELL_PREFIX(仅 bash)
6. 进程生成:
   - bash: spawn(command, [], { shell: true })
   - powershell: spawn(pwshPath, ['-NoProfile', '-NonInteractive', '-Command', cmd])
7. stdin 写入 JSON hook input
8. stdout 解析:
   a. 首行检测异步协议 {"async": true}
   b. Prompt Request 检测 {"prompt": "id", "message": "...", "options": [...]}
   c. JSON 输出验证(hookJSONOutputSchema)
   d. 非 JSON 作为纯文本处理

5.3 Prompt Hook 执行 (execPromptHook)

1. $ARGUMENTS 占位符替换
2. 构造 system prompt,要求返回 {"ok": true/false, "reason": "..."}
3. 调用 queryModelWithoutStreaming()
   - 使用 json_schema output format 强制 JSON 输出
   - 默认使用 small fast model (Haiku)
   - 默认超时 30 秒
4. 解析响应,验证 hookResponseSchema
5. ok=true --> success; ok=false --> blocking error

5.4 Agent Hook 执行 (execAgentHook)

1. $ARGUMENTS 占位符替换
2. 创建独立 agentId (hook-agent-UUID)
3. 注册 StructuredOutputTool 强制结构化输出
4. 配置 agent:
   - 过滤掉 agent 不允许使用的工具(防止嵌套 agent/plan mode)
   - 权限模式设为 dontAsk
   - 禁用 thinking
   - 允许读取 transcript 文件
5. 使用 query() 多轮执行(最多 50 轮)
6. 监听 structured_output attachment
7. ok=true --> success; ok=false --> blocking error

5.5 HTTP Hook 执行 (execHttpHook)

1. URL 允许列表检查 (allowedHttpHookUrls)
2. Header 环境变量插值 ($VAR_NAME, ${VAR_NAME})
   - 仅 allowedEnvVars 中列出的变量会被替换
   - 值经过 sanitizeHeaderValue 防止 CRLF 注入
3. 沙箱代理配置 (sandbox network proxy)
4. SSRF 防护 (ssrfGuardedLookup -- 阻止私有/链路本地 IP)
5. axios.post(url, jsonInput, { headers, proxy, lookup })
6. 响应解析:
   - 空响应体 --> 视为空 JSON 对象
   - 非 JSON --> 验证错误
   - JSON --> hookJSONOutputSchema 验证

6. Hook 输出协议

6.1 同步输出 Schema

// src/types/hooks.ts
export const syncHookResponseSchema = z.object({
  continue: z.boolean().optional(),       // false 则阻止 Claude 继续
  suppressOutput: z.boolean().optional(),  // 隐藏 stdout
  stopReason: z.string().optional(),       // continue=false 时的消息
  decision: z.enum(['approve', 'block']).optional(),
  reason: z.string().optional(),
  systemMessage: z.string().optional(),    // 警告消息
  hookSpecificOutput: z.union([
    // PreToolUse: 可修改权限决策和工具输入
    z.object({
      hookEventName: z.literal('PreToolUse'),
      permissionDecision: z.enum(['allow', 'deny', 'ask']).optional(),
      permissionDecisionReason: z.string().optional(),
      updatedInput: z.record(z.string(), z.unknown()).optional(),
      additionalContext: z.string().optional(),
    }),
    // PermissionRequest: 允许/拒绝权限
    z.object({
      hookEventName: z.literal('PermissionRequest'),
      decision: z.union([
        z.object({
          behavior: z.literal('allow'),
          updatedInput: z.record(z.string(), z.unknown()).optional(),
          updatedPermissions: z.array(permissionUpdateSchema()).optional(),
        }),
        z.object({
          behavior: z.literal('deny'),
          message: z.string().optional(),
          interrupt: z.boolean().optional(),
        }),
      ]),
    }),
    // SessionStart: 可设置 watchPaths 和 initialUserMessage
    z.object({
      hookEventName: z.literal('SessionStart'),
      additionalContext: z.string().optional(),
      initialUserMessage: z.string().optional(),
      watchPaths: z.array(z.string()).optional(),
    }),
    // PostToolUse: 可替换 MCP 工具输出
    z.object({
      hookEventName: z.literal('PostToolUse'),
      additionalContext: z.string().optional(),
      updatedMCPToolOutput: z.unknown().optional(),
    }),
    // PermissionDenied: 可请求重试
    z.object({
      hookEventName: z.literal('PermissionDenied'),
      retry: z.boolean().optional(),
    }),
    // WorktreeCreate: 返回 worktree 路径
    z.object({
      hookEventName: z.literal('WorktreeCreate'),
      worktreePath: z.string(),
    }),
    // ... 其他事件的特定输出
  ]).optional(),
})

6.2 异步输出 Schema

const asyncHookResponseSchema = z.object({
  async: z.literal(true),
  asyncTimeout: z.number().optional(),  // 默认 15000ms
})

export const hookJSONOutputSchema = z.union([
  asyncHookResponseSchema,
  syncHookResponseSchema,
])

6.3 类型守卫

export function isSyncHookJSONOutput(json: HookJSONOutput): json is SyncHookJSONOutput {
  return !('async' in json && json.async === true)
}

export function isAsyncHookJSONOutput(json: HookJSONOutput): json is AsyncHookJSONOutput {
  return 'async' in json && json.async === true
}

7. 权限 Hooks

7.1 PreToolUse 权限决策

PreToolUse hook 可以通过 hookSpecificOutput.permissionDecision 做出权限决策:

allow  --> 自动批准工具调用
deny   --> 阻止工具调用
ask    --> 显示权限对话框让用户决定

优先级规则 (在 executeHooks 的结果聚合中):

deny > ask > allow > (无决策)

即: 任何一个 hook 返回 deny,最终结果就是 denyask 优先于 allow,但不能覆盖 deny

7.2 PermissionRequest Hook

当权限对话框即将显示时触发,hook 可直接做出允许/拒绝决策:

// PermissionRequest hook 输出
{
  hookSpecificOutput: {
    hookEventName: 'PermissionRequest',
    decision: {
      behavior: 'allow',
      updatedInput: { ... },              // 可选: 修改工具输入
      updatedPermissions: [               // 可选: 更新权限规则
        { destination: 'session', value: 'Read(*.ts)' }
      ],
    }
  }
}
// 或
{
  hookSpecificOutput: {
    hookEventName: 'PermissionRequest',
    decision: {
      behavior: 'deny',
      message: '不允许执行此操作',
      interrupt: true,                    // 可选: 中断整个流程
    }
  }
}

7.3 权限上下文 (PermissionContext)

src/hooks/toolPermission/PermissionContext.ts 封装了权限决策流程:

function createPermissionContext(tool, input, toolUseContext, ...) {
  return {
    // 运行 PermissionRequest hooks
    async runHooks(permissionMode, suggestions, updatedInput, startTimeMs) {
      for await (const hookResult of executePermissionRequestHooks(...)) {
        if (hookResult.permissionRequestResult) {
          // allow --> handleHookAllow() 持久化权限更新
          // deny  --> buildDeny() 构建拒绝结果
        }
      }
      return null  // 无 hook 做出决策
    },

    // Hook 允许后的处理
    async handleHookAllow(finalInput, permissionUpdates, startTimeMs) {
      await this.persistPermissions(permissionUpdates)
      return this.buildAllow(finalInput, {
        decisionReason: { type: 'hook', hookName: 'PermissionRequest' },
      })
    },

    // 持久化权限更新
    async persistPermissions(updates: PermissionUpdate[]) { ... },

    // 构建允许/拒绝结果
    buildAllow(updatedInput, opts?): PermissionAllowDecision { ... },
    buildDeny(message, reason): PermissionDenyDecision { ... },
  }
}

8. 异步 Hook 机制

8.1 两种异步模式

1. async: true(配置级别)

在 settings.json 中设置 "async": true:

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write",
      "hooks": [{
        "type": "command",
        "command": "npx prettier --write $FILE",
        "async": true
      }]
    }]
  }
}

2. 运行时协商(首行输出)

Hook 进程在首行输出 {"async": true} 来请求异步执行:

#!/bin/bash
echo '{"async": true, "asyncTimeout": 30000}'
# 后续长时间运行的任务...
long_running_check

8.2 asyncRewake 模式

asyncRewake 是特殊的异步模式,当 hook 进程以 exit code 2 退出时, 会将 stderr 作为 task-notification 注入消息队列,唤醒模型继续处理:

// src/utils/hooks.ts:executeInBackground()
if (asyncRewake) {
  void shellCommand.result.then(async result => {
    if (result.code === 2) {
      enqueuePendingNotification({
        value: wrapInSystemReminder(
          `Stop hook blocking error from command "${hookName}": ${stderr || stdout}`,
        ),
        mode: 'task-notification',
      })
    }
  })
  return true
}

8.3 AsyncHookRegistry

src/utils/hooks/AsyncHookRegistry.ts 管理所有后台运行的异步 hook:

type PendingAsyncHook = {
  processId: string
  hookId: string
  hookName: string
  hookEvent: HookEvent | 'StatusLine' | 'FileSuggestion'
  toolName?: string
  pluginId?: string
  startTime: number
  timeout: number                  // 默认 15 秒
  command: string
  responseAttachmentSent: boolean  // 是否已发送响应
  shellCommand?: ShellCommand
  stopProgressInterval: () => void
}

// 主要 API
registerPendingAsyncHook(...)       // 注册异步 hook
getPendingAsyncHooks()              // 获取未完成的 hook
checkForAsyncHookResponses()        // 轮询已完成的 hook
removeDeliveredAsyncHooks(ids)      // 清理已处理的 hook
finalizePendingAsyncHooks()         // Session 结束时清理所有

checkForAsyncHookResponses() 在每次 query loop 迭代中被调用, 检查是否有异步 hook 完成,如果完成则解析其输出并注入到消息流中。

8.4 once 标志

once: true 时,hook 执行一次后自动从配置中移除。 这在 session hook 和 skill hook 中常见,用于一次性初始化逻辑。


9. Hook 事件广播系统

src/utils/hooks/hookEvents.ts 提供了独立于消息流的事件广播:

// 事件类型
type HookStartedEvent = {
  type: 'started'
  hookId: string; hookName: string; hookEvent: string
}

type HookProgressEvent = {
  type: 'progress'
  hookId: string; hookName: string; hookEvent: string
  stdout: string; stderr: string; output: string
}

type HookResponseEvent = {
  type: 'response'
  hookId: string; hookName: string; hookEvent: string
  output: string; stdout: string; stderr: string
  exitCode?: number
  outcome: 'success' | 'error' | 'cancelled'
}

// 注册处理器(SDK 消费)
registerHookEventHandler(handler: HookEventHandler | null)

// 发射事件
emitHookStarted(hookId, hookName, hookEvent)
emitHookProgress(data)
emitHookResponse(data)

// 周期性进度报告(异步 hook)
startHookProgressInterval(params): () => void  // 返回清理函数

事件过滤: SessionStartSetup 事件始终广播; 其他事件需要通过 setAllHookEventsEnabled(true) 启用 (SDK includeHookEvents 选项或 CLAUDE_CODE_REMOTE 模式)。

缓冲机制: 在 handler 注册前,事件缓存在 pendingEvents 数组中 (最多 100 条),handler 注册后立即 flush。


10. Session Hooks

src/utils/hooks/sessionHooks.ts 管理运行时动态注册的 hook。 这些 hook 存储在内存中,不持久化到 settings.json。

10.1 存储结构

type SessionStore = {
  hooks: {
    [event in HookEvent]?: SessionHookMatcher[]
  }
}

// 使用 Map 而非 Record,避免 O(N^2) 复制开销
type SessionHooksState = Map<string, SessionStore>

10.2 主要 API

// 添加 command/prompt/http/agent hook
addSessionHook(setAppState, sessionId, event, matcher, hook, onHookSuccess?, skillRoot?)

// 添加 function hook(内存回调)
addFunctionHook(setAppState, sessionId, event, matcher, callback, errorMessage, options?)
  --> returns hookId

// 移除 hook
removeSessionHook(setAppState, sessionId, event, hook)
removeFunctionHook(setAppState, sessionId, event, hookId)

// 查询
getSessionHooks(appState, sessionId, event?)     // 排除 function hooks
getSessionFunctionHooks(appState, sessionId, event?)
getSessionHookCallback(appState, sessionId, event, matcher, hook)

// 清理
clearSessionHooks(setAppState, sessionId)

10.3 使用场景

  • Skill Hooks: Skill 注册的 frontmatter hooks 通过 addSessionHook 添加
  • Structured Output 强制: registerStructuredOutputEnforcement 注册 Stop hook 确保 agent hook 调用 SyntheticOutputTool
  • Agent Frontmatter Hooks: Agent 定义中的 hooks 在运行时注册为 session hooks

11. Hook 配置快照

src/utils/hooks/hooksConfigSnapshot.ts 在应用启动时捕获一份 hook 配置快照:

// 应用启动时调用
captureHooksConfigSnapshot()

// 设置修改后更新(会先 resetSettingsCache)
updateHooksConfigSnapshot()

// 获取当前快照
getHooksConfigFromSnapshot(): HooksSettings | null

配置来源优先级:

policySettings.disableAllHooks === true  -->  返回空(全部禁用)
policySettings.allowManagedHooksOnly === true  -->  仅返回 policySettings.hooks
isRestrictedToPluginOnly('hooks')  -->  仅返回 policySettings.hooks
mergedSettings.disableAllHooks === true  -->  仅返回 policySettings.hooks(非管理策略不能禁用管理 hooks)
默认  -->  返回 mergedSettings.hooks(所有来源合并)

12. Notification Hooks (notifs/)

src/hooks/notifs/ 目录包含 React hooks,用于 UI 通知:

Hook 用途
useSettingsErrors 设置文件错误通知
useRateLimitWarningNotification 速率限制警告
useDeprecationWarningNotification 弃用警告
useStartupNotification 启动通知
useFastModeNotification 快速模式通知
useLspInitializationNotification LSP 初始化通知
useMcpConnectivityStatus MCP 连接状态
usePluginAutoupdateNotification 插件自动更新通知
usePluginInstallationStatus 插件安装状态
useAutoModeUnavailableNotification 自动模式不可用通知
useModelMigrationNotifications 模型迁移通知

这些是 React hooks (useXxx),与 settings.json 中的 Hooks 系统是不同层面的概念。 它们是 UI 层面的通知组件,不参与 hook 执行引擎。

Hooks 系统中的 Notification 事件用于拦截通知发送,通过 executeNotificationHooks() 触发,matcher 匹配 notification_type 字段。


13. settings.json 配置格式

13.1 基本结构

// ~/.claude/settings.json 或 .claude/settings.json
{
  "hooks": {
    "<HookEvent>": [
      {
        "matcher": "<pattern>",   // 可选,匹配目标
        "hooks": [
          {
            "type": "command",
            "command": "echo hello",
            "timeout": 30,
            "once": false,
            "async": false
          }
        ]
      }
    ]
  }
}

13.2 完整示例

{
  "hooks": {
    // PreToolUse: 在 Write/Edit 前运行 lint
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":\"Remember to follow project style guide\"}}'",
            "timeout": 10
          }
        ]
      }
    ],

    // PostToolUse: 编辑后自动格式化
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write \"$(echo $ARGUMENTS | jq -r '.tool_input.file_path // .tool_input.file_name // empty')\"",
            "async": true
          }
        ]
      }
    ],

    // Stop: 使用 LLM 验证任务完成
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Verify that all modified files have proper error handling. $ARGUMENTS",
            "model": "claude-sonnet-4-6"
          }
        ]
      }
    ],

    // Stop: 使用 Agent 深度验证
    "Stop": [
      {
        "hooks": [
          {
            "type": "agent",
            "prompt": "Verify that unit tests pass and coverage is above 80%.",
            "timeout": 120
          }
        ]
      }
    ],

    // PermissionRequest: 自动批准 Read 操作
    "PermissionRequest": [
      {
        "matcher": "Read",
        "hooks": [
          {
            "type": "command",
            "command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PermissionRequest\",\"decision\":{\"behavior\":\"allow\"}}}'"
          }
        ]
      }
    ],

    // HTTP hook: 通知外部系统
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "http",
            "url": "https://hooks.example.com/claude-session",
            "headers": {
              "Authorization": "Bearer $API_TOKEN"
            },
            "allowedEnvVars": ["API_TOKEN"]
          }
        ]
      }
    ],

    // CwdChanged: 目录切换时加载环境变量
    "CwdChanged": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "if [ -f .envrc ]; then echo 'export FOO=bar' > \"$CLAUDE_ENV_FILE\"; fi"
          }
        ]
      }
    ],

    // FileChanged: 监视配置文件变更
    "FileChanged": [
      {
        "matcher": ".envrc|.env",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Config file changed, reloading environment'"
          }
        ]
      }
    ]
  },

  // HTTP hook URL 允许列表
  "allowedHttpHookUrls": [
    "https://hooks.example.com/*",
    "https://api.mycompany.com/webhooks/*"
  ],

  // HTTP hook 允许的环境变量
  "httpHookAllowedEnvVars": ["API_TOKEN", "WEBHOOK_SECRET"],

  // 管理策略选项
  "allowManagedHooksOnly": false,
  "disableAllHooks": false
}

13.3 配置来源

Hook 配置从多个来源合并:

来源 路径 优先级
User Settings ~/.claude/settings.json 最高(用户级)
Project Settings .claude/settings.json 项目级
Local Settings .claude/settings.local.json 本地覆盖
Policy Settings 管理策略 策略级(可限制其他来源)
Plugin Hooks ~/.claude/plugins/*/hooks/hooks.json 插件级
Session Hooks 内存 运行时动态
Built-in Hooks 代码内注册 内部

14. 安全机制

14.1 工作区信任检查

所有 hook 执行前必须通过信任检查:

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

这是一个纵深防御措施,防止未经信任的工作区执行任意命令。 历史漏洞包括: SessionEnd hooks 在用户拒绝信任对话框时执行、 SubagentStop hooks 在信任建立前执行。

14.2 HTTP Hook 安全

  • URL 允许列表: allowedHttpHookUrls 限制可访问的 URL 模式
  • SSRF 防护: ssrfGuardedLookup 阻止私有 IP 和链路本地地址
  • Header 注入防护: sanitizeHeaderValue 移除 CR/LF/NUL 字节
  • 环境变量控制: 仅 allowedEnvVars 中的变量会被插值
  • 沙箱代理: sandbox 模式下请求经过代理,由代理强制域名白名单

14.3 管理策略控制

// policySettings.disableAllHooks: 完全禁用所有 hook(包括管理策略自身的)
shouldDisableAllHooksIncludingManaged(): boolean

// policySettings.allowManagedHooksOnly: 仅允许管理策略定义的 hook
shouldAllowManagedHooksOnly(): boolean

15. Hook 结果聚合

15.1 HookResult 类型

export interface HookResult {
  hook: HookCommand | HookCallback | FunctionHook
  outcome: 'success' | 'blocking' | 'non_blocking_error' | 'cancelled'
  message?: HookResultMessage
  systemMessage?: string
  blockingError?: HookBlockingError
  preventContinuation?: boolean
  stopReason?: string
  permissionBehavior?: 'ask' | 'deny' | 'allow' | 'passthrough'
  hookPermissionDecisionReason?: string
  additionalContext?: string
  updatedInput?: Record<string, unknown>
  updatedMCPToolOutput?: unknown
  permissionRequestResult?: PermissionRequestResult
  elicitationResponse?: ElicitationResponse
  watchPaths?: string[]
  retry?: boolean
}

15.2 AggregatedHookResult 类型

export type AggregatedHookResult = {
  message?: HookResultMessage
  blockingError?: HookBlockingError
  preventContinuation?: boolean
  stopReason?: string
  permissionBehavior?: PermissionResult['behavior']
  hookPermissionDecisionReason?: string
  hookSource?: string
  additionalContexts?: string[]
  updatedInput?: Record<string, unknown>
  updatedMCPToolOutput?: unknown
  permissionRequestResult?: PermissionRequestResult
  watchPaths?: string[]
  elicitationResponse?: ElicitationResponse
  elicitationResultResponse?: ElicitationResponse
  retry?: boolean
}

15.3 聚合规则

executeHooks() 是一个 async generator,逐个 yield AggregatedHookResult。 权限行为按优先级聚合: deny > ask > allow > passthrough。 blocking error 立即 yield,不阻止后续 hook 执行。 additionalContext 会追加为 hook_additional_context 系统消息。


16. Prompt Elicitation 协议

Command Hook 支持交互式 prompt 请求,用于在 hook 执行过程中向用户提问:

// Hook 进程输出 JSON 请求:
{
  "prompt": "request-id",
  "message": "请选择操作方式:",
  "options": [
    { "key": "1", "label": "方式 A", "description": "使用方式 A 处理" },
    { "key": "2", "label": "方式 B" }
  ]
}

// 系统通过 stdin 返回用户选择:
{
  "prompt_response": "request-id",
  "selected": "1"
}

请求和响应通过 stdin/stdout 序列化传输,异步 prompt 处理链保证顺序。


17. 遥测与可观测性

Hook 执行产生以下遥测事件:

事件 触发点
tengu_run_hook Hook 开始执行
tengu_repl_hook_finished Hook 批次执行完成
tengu_agent_stop_hook_success Agent hook 成功
tengu_agent_stop_hook_error Agent hook 错误
tengu_agent_stop_hook_max_turns Agent hook 达到最大轮数
hook_execution_start (OTEL) Beta tracing -- hook 开始
hook_execution_complete (OTEL) Beta tracing -- hook 完成

此外,hook_duration_ms 指标通过 StatsStore 记录, addToTurnHookDuration 累计到每轮的 hook 执行时间。