Claude Code 源码分析Claude Code 源码分析
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
  • 概览

    • Claude Code 源码分析
    • 源码统计
    • CodeGraph 图谱
  • 架构

    • 系统架构
    • UML 图表索引
    • 查询引擎
    • 核心流程
    • 消息系统
    • 状态管理
  • 功能模块

    • 工具系统
    • 斜杠命令
    • 服务层
    • MCP 协议
    • Skills 技能
    • 子代理系统
  • 分层深度

    • 入口层
    • UI / Ink 层
    • utils 基础设施
    • 桥接 / 远程
    • 上下文压缩
  • 原理与安全

    • 底层原理
    • 技术难点
    • 权限与安全
    • 内部机制
    • 遥测与分析
  • 深度专题

    • Hooks 系统
    • 插件系统
    • 记忆系统
    • API 通信层
    • Ink 终端 UI
    • 认证系统
    • 构建与发布
    • 术语表
  • 调用分析

    • 调用链分析
    • 核心文件索引
  • 模块详解

    • utils

      • 模块: utils
      • messages · 消息工厂与规范化
      • session-storage · JSONL 会话持久化
      • permissions · 工具权限决策
      • shell-hooks · 用户 Shell Hook 系统
    • components

      • 模块: components
      • REPL · 主屏编排
      • messages · 消息行渲染
      • PermissionRequest · 权限弹窗
      • PromptInput · 底部输入
    • services

      • 模块: services
      • api-claude · Anthropic API 流式与重试
      • mcp-client · MCP 连接与工具调用
      • compact · 上下文压缩与自动触发
      • analytics · GrowthBook、Datadog 与 1P 事件
    • tools

      • 模块: tools
      • tool-interface · Tool 契约与注册表
      • bash-tool · Shell 执行与权限
      • streaming-executor · 流式工具并发调度
      • agent-tool · 子 Agent 委派
    • commands

      • 模块: commands
      • command-registry · commands.ts 注册与分派
      • model-command · /model 模型选择
      • mcp-commands · /mcp 服务器管理
      • compact-memory-commands · /compact 与 /memory
    • ink

      • 模块: ink
      • Ink 渲染管线 · Screen 与终端输出
      • 终端事件 · resize、paste、stdin
      • Ink Hooks · 输入、搜索、终端状态
      • Ink 组件 · Box、Text、ScrollBox 原语
    • hooks

      • 模块: hooks
      • useCanUseTool · 权限 UI 接缝
      • 输入与快捷键 Hook
      • 合并态 Hook(MCP + 本地)
      • notifs 通知 Hook
    • bridge

      • 模块: bridge
      • repl-bridge · REPL 桥初始化与传输
      • bridge-messaging · 桥消息路由与入站处理
      • remote-bridge-core · env-less 核心与守护主循环
      • bridge-permissions-ui · 权限、API 与 TUI
    • cli

      • 模块: cli
      • Structured IO · NDJSON SDK 协议
      • CLI Transports · Session Ingress 传输层
      • CLI Handlers · 子命令懒加载实现
      • Update & Upload · 自更新与串行上传原语
    • screens

      • 模块: screens
      • REPL 屏 · Screen 类型与顶层路由
      • ResumeConversation · 会话恢复选择器
      • Doctor · 安装诊断全屏
    • entrypoints

      • 模块: entrypoints
      • cli-entrypoint · Bootstrap 与快路径
      • sdk-types · core / control / runtime 类型体系
      • mcp-entrypoint · MCP stdio 服务器
      • sandbox-types · 沙箱配置单一真相源
    • skills

      • 模块: skills
      • skills-loading · 磁盘加载与 bundled 注册表
      • bundled-skills · 内置 skill 与 initBundledSkills
      • mcp-skills · MCP prompt 转 skill
      • skill-tool-integration · SkillTool 与命令注册
    • types

      • 模块: types
      • message-types · Message 联合与 content blocks
      • tool-permission-types · Tool、Permission、Command 类型
      • api-sdk-types · API 与 Hooks 协议类型
      • misc-types · ids、plugin、generated 与其余类型
    • tasks

      • 模块: tasks
      • local-agent-task · 本地 Agent 与主会话后台化
      • remote-agent-task · 远程 CCR 与 In-Process Teammate
      • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
      • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybindings

      • 模块: keybindings
      • keybinding-registry · 注册、Provider 与 useKeybinding
      • default-bindings · 默认键位表与平台差异
      • command-bindings · command:* 动态斜杠命令绑定
      • vim-bindings · Vim 模式与 keybindings 边界
    • memdir

      • 模块: memdir
      • memdir-core · 路径、加载与 MEMORY.md
      • memory-extraction · extractMemories 与 SessionMemory
      • memdir-commands · /memory、/remember 与命令集成
    • state

      • 模块: state
      • app-state-core · store、AppState 类型与 Provider
      • app-state-selectors · selectors 与 onChangeAppState
      • teammate-state · 队友视图与 swarm 状态
      • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query

      • 模块: query
      • query config 与 deps · 配置快照与依赖注入
      • query tokenBudget · +500k 自动续跑
      • query transitions · Continue / Terminal 状态机
      • query stopHooks · Stop 事件与 turn 结束编排
  • 模块详解(扩展)

    • messages · 消息工厂与规范化
    • session-storage · JSONL 会话持久化
    • permissions · 工具权限决策
    • shell-hooks · 用户 Shell Hook 系统
    • REPL · 主屏编排
    • messages · 消息行渲染
    • PermissionRequest · 权限弹窗
    • PromptInput · 底部输入
    • api-claude · Anthropic API 流式与重试
    • mcp-client · MCP 连接与工具调用
    • compact · 上下文压缩与自动触发
    • analytics · GrowthBook、Datadog 与 1P 事件
    • tool-interface · Tool 契约与注册表
    • bash-tool · Shell 执行与权限
    • streaming-executor · 流式工具并发调度
    • agent-tool · 子 Agent 委派
    • command-registry · commands.ts 注册与分派
    • model-command · /model 模型选择
    • mcp-commands · /mcp 服务器管理
    • compact-memory-commands · /compact 与 /memory
    • Ink 渲染管线 · Screen 与终端输出
    • 终端事件 · resize、paste、stdin
    • Ink Hooks · 输入、搜索、终端状态
    • Ink 组件 · Box、Text、ScrollBox 原语
    • useCanUseTool · 权限 UI 接缝
    • 输入与快捷键 Hook
    • 合并态 Hook(MCP + 本地)
    • notifs 通知 Hook
    • repl-bridge · REPL 桥初始化与传输
    • bridge-messaging · 桥消息路由与入站处理
    • remote-bridge-core · env-less 核心与守护主循环
    • bridge-permissions-ui · 权限、API 与 TUI
    • Structured IO · NDJSON SDK 协议
    • CLI Transports · Session Ingress 传输层
    • CLI Handlers · 子命令懒加载实现
    • Update & Upload · 自更新与串行上传原语
    • REPL 屏 · Screen 类型与顶层路由
    • ResumeConversation · 会话恢复选择器
    • Doctor · 安装诊断全屏
    • cli-entrypoint · Bootstrap 与快路径
    • sdk-types · core / control / runtime 类型体系
    • mcp-entrypoint · MCP stdio 服务器
    • sandbox-types · 沙箱配置单一真相源
    • skills-loading · 磁盘加载与 bundled 注册表
    • bundled-skills · 内置 skill 与 initBundledSkills
    • mcp-skills · MCP prompt 转 skill
    • skill-tool-integration · SkillTool 与命令注册
    • message-types · Message 联合与 content blocks
    • tool-permission-types · Tool、Permission、Command 类型
    • api-sdk-types · API 与 Hooks 协议类型
    • misc-types · ids、plugin、generated 与其余类型
    • local-agent-task · 本地 Agent 与主会话后台化
    • remote-agent-task · 远程 CCR 与 In-Process Teammate
    • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
    • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybinding-registry · 注册、Provider 与 useKeybinding
    • default-bindings · 默认键位表与平台差异
    • command-bindings · command:* 动态斜杠命令绑定
    • vim-bindings · Vim 模式与 keybindings 边界
    • memdir-core · 路径、加载与 MEMORY.md
    • memory-extraction · extractMemories 与 SessionMemory
    • memdir-commands · /memory、/remember 与命令集成
    • app-state-core · store、AppState 类型与 Provider
    • app-state-selectors · selectors 与 onChangeAppState
    • teammate-state · 队友视图与 swarm 状态
    • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query config 与 deps · 配置快照与依赖注入
    • query tokenBudget · +500k 自动续跑
    • query transitions · Continue / Terminal 状态机
    • query stopHooks · Stop 事件与 turn 结束编排
  • 工具详解

    • tool-interface · Tool 契约与注册表
    • tool-permission-types · Tool、Permission、Command 类型
    • 工具: Bash
    • 工具: PowerShell
    • 工具: Agent
    • 工具: LSP
    • 工具: FileEdit
    • 工具: FileRead
    • 工具: Skill
    • 工具: WebFetch
    • 工具: MCP
    • 工具: SendMessage
    • 工具: FileWrite
    • 工具: Config
    • 工具: Grep
    • 工具: Brief
    • 工具: ExitPlanMode
    • 工具: ToolSearch
    • 工具: NotebookEdit
    • 工具: TaskOutput
    • 工具: WebSearch
    • 工具: ScheduleCron

本章总览

utils/hooks.ts(约 5000 行)实现 Claude Code 的 用户可配置 Shell Hook(PreToolUse、PostToolUse、Stop、SessionStart、PermissionRequest 等),与 src/hooks/ React 目录完全不同。配置来自 settings 与插件;getMatchingHooks 按事件与 matcher 过滤;executeHooks 作为 async generator 统一执行命令、prompt hook、callback,并处理信任对话框、超时、阻塞退出码 2。本章是扩展 Claude Code 行为的主入口文档。

学完本章你应该能

  • 列举至少 8 种 hook_event_name 及 matchQuery 来源
  • 解释 executeHooks 的安全门(trust、CLAUDE_CODE_SIMPLE、shouldDisableAllHooks)
  • 说明 executePreToolUse 与 executePermissionRequestHooks 如何委托 executeHooks
  • 理解 blocking 与 exit code 2 的语义
  • 区分 executeHooks 与 executeHooksOutsideREPL 的使用场景

核心概念(先读懂这些)

Hook 是用户代码,不是内部回调

settings 中 hooks 数组可定义 matcher 与 command。插件、Skill 也可注入 hookMatchers。isInternalHook 区分内部 callback(如 sessionFileAccessHooks)与用户 shell 命令。内部 hook 走 fast-path,跳过 span/progress,微秒级;用户 hook 可能启动子进程,必须限超时并尊重 AbortSignal。

信任模型:shouldSkipHookDueToTrust

交互模式下 所有 hook 需要用户接受 workspace trust,否则 RCE 风险(历史漏洞:SessionEnd 在未信任时执行)。非交互 SDK 模式隐式信任。executeHooks 开头统一检查,避免每个 export 重复遗漏。

executeHooks 与 OutsideREPL

executeHooks 向 REPL yield AggregatedHookResult,可把 hook 输出变成模型可见消息。executeHooksOutsideREPL 在 SessionEnd、PreCompact 等无 REPL 场景运行,hardcode main sessionId,且 function hook 在 OutsideREPL 路径会抛错(仅 Stop 等允许 function hook)。读源码时先看调用方再决定跟哪条路径。

建议学习步骤

  1. 阅读 shouldSkipHookDueToTrust 与 createBaseHookInput
  2. 阅读 getMatchingHooks 的 matchQuery switch
  3. 阅读 executeHooks 主体(超时、trust、匹配)
  4. 阅读 executePreToolUseHooks 与 executePermissionRequestHooks
  5. 对照 settings 示例配置一条 PreToolUse 命令

常见误区

注意

目录 src/hooks/ 与本文件无关;搜 Hook 时限定 utils/hooks.ts

注意

PermissionRequest hook 可改权限,但 policy ConfigChange 不可被 block

注意

PostToolUse 与 PostToolUseFailure 的 matcher 都是 tool_name

命名澄清与配置来源

Claude Code 存在三套「hook」概念:

名称位置用途
Shell Hookutils/hooks.ts用户 settings / 插件命令
React Hookssrc/hooks/Ink UI 状态
Permission hooksuseCanUseTool 调 utils/hooks.tsPreToolUse / PermissionRequest 等

配置由 getHooksConfig(appState, sessionId, hookEvent) 读取,hooksConfigManager.ts 注释要求与 getMatchingHooks 的 matchQuery 规则保持同步。

典型 settings 片段(概念性):

"hooks": {
  "PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": ".claude/hooks/lint.sh" }] }]
}

信任检查与 base hook input

shouldSkipHookDueToTrust(约 286 行):

  • 非交互会话 → 不跳过
  • 交互且未接受 trust dialog → 跳过所有 hook

createBaseHookInput 构造各事件共享字段:session_id、transcript_path、cwd、permission_mode、agent_id/agent_type。transcript_path 使用 getTranscriptPathForSession,与 session-storage 章的 sessionProjectDir 一致性问题直接相关。

安全: executeHooks 在 trust 失败时 logForDebugging 并 return,不抛异常,避免阻断主流程。

源码引用: src/utils/hooks.ts · 第 286–319 行(共 5023 行)

 286| export function shouldSkipHookDueToTrust(): boolean {
 287|   // In non-interactive mode (SDK), trust is implicit - always execute
 288|   const isInteractive = !getIsNonInteractiveSession()
 289|   if (!isInteractive) {
 290|     return false
 291|   }
 292| 
 293|   // In interactive mode, ALL hooks require trust
 294|   const hasTrust = checkHasTrustDialogAccepted()
 295|   return !hasTrust
 296| }
 297| 
 298| /**
 299|  * Creates the base hook input that's common to all hook types
 300|  */
 301| export function createBaseHookInput(
 302|   permissionMode?: string,
 303|   sessionId?: string,
 304|   // Typed narrowly (not ToolUseContext) so callers can pass toolUseContext
 305|   // directly via structural typing without this function depending on Tool.ts.
 306|   agentInfo?: { agentId?: string; agentType?: string },
 307| ): {
 308|   session_id: string
 309|   transcript_path: string
 310|   cwd: string
 311|   permission_mode?: string
 312|   agent_id?: string
 313|   agent_type?: string
 314| } {
 315|   const resolvedSessionId = sessionId ?? getSessionId()
 316|   // agent_type: subagent's type (from toolUseContext) takes precedence over
 317|   // the session's --agent flag. Hooks use agent_id presence to distinguish
 318|   // subagent calls from main-thread calls in a --agent session.
 319|   const resolvedAgentType = agentInfo?.agentType ?? getMainThreadAgentType()

getMatchingHooks:事件与 matcher

getMatchingHooks(约 1603 行)根据 hook_event_name 计算 matchQuery:

  • PreToolUse / PostToolUse / PostToolUseFailure / PermissionRequest / PermissionDenied → tool_name
  • SessionStart → source
  • Notification → notification_type
  • SubagentStart / SubagentStop → agent_type
  • FileChanged → basename(file_path)
  • 等等

然后用 matchesPattern(matchQuery, matcher.matcher) 过滤。插件 hook 附带 pluginRoot/pluginId;Skill hook 带 skillRoot。

verbose 日志帮助排查「配置了但未触发」:先看 matcher 是否过严,再看 sessionId 是否一致。

源码引用: src/utils/hooks.ts · 第 1603–1686 行(共 5023 行)

1603| export async function getMatchingHooks(
1604|   appState: AppState | undefined,
1605|   sessionId: string,
1606|   hookEvent: HookEvent,
1607|   hookInput: HookInput,
1608|   tools?: Tools,
1609| ): Promise<MatchedHook[]> {
1610|   try {
1611|     const hookMatchers = getHooksConfig(appState, sessionId, hookEvent)
1612| 
1613|     // If you change the criteria below, then you must change
1614|     // src/utils/hooks/hooksConfigManager.ts as well.
1615|     let matchQuery: string | undefined = undefined
1616|     switch (hookInput.hook_event_name) {
1617|       case 'PreToolUse':
1618|       case 'PostToolUse':
1619|       case 'PostToolUseFailure':
1620|       case 'PermissionRequest':
1621|       case 'PermissionDenied':
1622|         matchQuery = hookInput.tool_name
1623|         break
1624|       case 'SessionStart':
1625|         matchQuery = hookInput.source
1626|         break
1627|       case 'Setup':
1628|         matchQuery = hookInput.trigger
1629|         break
1630|       case 'PreCompact':
1631|       case 'PostCompact':
1632|         matchQuery = hookInput.trigger
1633|         break
1634|       case 'Notification':
1635|         matchQuery = hookInput.notification_type
1636|         break
1637|       case 'SessionEnd':
1638|         matchQuery = hookInput.reason
1639|         break
1640|       case 'StopFailure':
1641|         matchQuery = hookInput.error
1642|         break
1643|       case 'SubagentStart':
1644|         matchQuery = hookInput.agent_type
1645|         break
1646|       case 'SubagentStop':
1647|         matchQuery = hookInput.agent_type
1648|         break
1649|       case 'TeammateIdle':
1650|       case 'TaskCreated':
1651|       case 'TaskCompleted':
1652|         break
1653|       case 'Elicitation':
1654|         matchQuery = hookInput.mcp_server_name
1655|         break
1656|       case 'ElicitationResult':
1657|         matchQuery = hookInput.mcp_server_name
1658|         break
1659|       case 'ConfigChange':
1660|         matchQuery = hookInput.source
1661|         break
1662|       case 'InstructionsLoaded':
1663|         matchQuery = hookInput.load_reason
1664|         break
1665|       case 'FileChanged':
1666|         matchQuery = basename(hookInput.file_path)
1667|         break
1668|       default:
1669|         break
1670|     }
1671| 
1672|     logForDebugging(
1673|       `Getting matching hook commands for ${hookEvent} with query: ${matchQuery}`,
1674|       { level: 'verbose' },
1675|     )
1676|     logForDebugging(`Found ${hookMatchers.length} hook matchers in settings`, {
1677|       level: 'verbose',
1678|     })
1679| 
1680|     // Extract hooks with their plugin context (if any)
1681|     const filteredMatchers = matchQuery
1682|       ? hookMatchers.filter(
1683|           matcher =>
1684|             !matcher.matcher || matchesPattern(matchQuery, matcher.matcher),
1685|         )
1686|       : hookMatchers

executeHooks 核心循环

executeHooks(约 1952 行)是执行引擎心脏:

入口守卫:

  • shouldDisableAllHooksIncludingManaged()
  • CLAUDE_CODE_SIMPLE 环境变量
  • shouldSkipHookDueToTrust()

准备:

  • hookName = event 或 event:matchQuery
  • boundRequestPrompt 绑定 UI 回调(prompt hook)
  • getMatchingHooks → 空则 return
  • signal aborted 则 return

用户 hook:

  • 记录 tengu_run_hook 分析事件
  • 内部 hook 批量 fast-path(callback 类型)

执行:

  • 子进程 command、prompt、function 等类型分支(后续行)
  • 解析 JSON 输出、阻塞错误 exit 2
  • yield AggregatedHookResult 给 REPL

超时默认 TOOL_HOOK_EXECUTION_TIMEOUT_MS,调用方可覆盖。

源码引用: src/utils/hooks.ts · 第 1941–2048 行(共 5023 行)

1941| /**
1942|  * Common logic for executing hooks
1943|  * @param hookInput The structured hook input that will be validated and converted to JSON
1944|  * @param toolUseID The ID for tracking this hook execution
1945|  * @param matchQuery The query to match against hook matchers
1946|  * @param signal Optional AbortSignal to cancel hook execution
1947|  * @param timeoutMs Optional timeout in milliseconds for hook execution
1948|  * @param toolUseContext Optional ToolUseContext for prompt-based hooks (required if using prompt hooks)
1949|  * @param messages Optional conversation history for prompt/function hooks
1950|  * @returns Async generator that yields progress messages and hook results
1951|  */
1952| async function* executeHooks({
1953|   hookInput,
1954|   toolUseID,
1955|   matchQuery,
1956|   signal,
1957|   timeoutMs = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
1958|   toolUseContext,
1959|   messages,
1960|   forceSyncExecution,
1961|   requestPrompt,
1962|   toolInputSummary,
1963| }: {
1964|   hookInput: HookInput
1965|   toolUseID: string
1966|   matchQuery?: string
1967|   signal?: AbortSignal
1968|   timeoutMs?: number
1969|   toolUseContext?: ToolUseContext
1970|   messages?: Message[]
1971|   forceSyncExecution?: boolean
1972|   requestPrompt?: (
1973|     sourceName: string,
1974|     toolInputSummary?: string | null,
1975|   ) => (request: PromptRequest) => Promise<PromptResponse>
1976|   toolInputSummary?: string | null
1977| }): AsyncGenerator<AggregatedHookResult> {
1978|   if (shouldDisableAllHooksIncludingManaged()) {
1979|     return
1980|   }
1981| 
1982|   if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
1983|     return
1984|   }
1985| 
1986|   const hookEvent = hookInput.hook_event_name
1987|   const hookName = matchQuery ? `${hookEvent}:${matchQuery}` : hookEvent
1988| 
1989|   // Bind the prompt callback to this hook's name and tool input summary so the UI can display context
1990|   const boundRequestPrompt = requestPrompt?.(hookName, toolInputSummary)
1991| 
1992|   // SECURITY: ALL hooks require workspace trust in interactive mode
1993|   // This centralized check prevents RCE vulnerabilities for all current and future hooks
1994|   if (shouldSkipHookDueToTrust()) {
1995|     logForDebugging(
1996|       `Skipping ${hookName} hook execution - workspace trust not accepted`,
1997|     )
1998|     return
1999|   }
2000| 
2001|   const appState = toolUseContext ? toolUseContext.getAppState() : undefined
2002|   // Use the agent's session ID if available, otherwise fall back to main session
2003|   const sessionId = toolUseContext?.agentId ?? getSessionId()
2004|   const matchingHooks = await getMatchingHooks(
2005|     appState,
2006|     sessionId,
2007|     hookEvent,
2008|     hookInput,
2009|     toolUseContext?.options?.tools,
2010|   )
2011|   if (matchingHooks.length === 0) {
2012|     return
2013|   }
2014| 
2015|   if (signal?.aborted) {
2016|     return
2017|   }
2018| 
2019|   const userHooks = matchingHooks.filter(h => !isInternalHook(h))
2020|   if (userHooks.length > 0) {
2021|     const pluginHookCounts = getPluginHookCounts(userHooks)
2022|     const hookTypeCounts = getHookTypeCounts(userHooks)
2023|     logEvent(`tengu_run_hook`, {
2024|       hookName:
2025|         hookName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
2026|       numCommands: userHooks.length,
2027|       hookTypeCounts: jsonStringify(
2028|         hookTypeCounts,
2029|       ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
2030|       ...(pluginHookCounts && {
2031|         pluginHookCounts: jsonStringify(
2032|           pluginHookCounts,
2033|         ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
2034|       }),
2035|     })
2036|   } else {
2037|     // Fast-path: all hooks are internal callbacks (sessionFileAccessHooks,
2038|     // attributionHooks). These return {} and don't use the abort signal, so we
2039|     // can skip span/progress/abortSignal/processHookJSONOutput/resultLoop.
2040|     // Measured: 6.01µs → ~1.8µs per PostToolUse hit (-70%).
2041|     const batchStartTime = Date.now()
2042|     const context = toolUseContext
2043|       ? {
2044|           getAppState: toolUseContext.getAppState,
2045|           updateAttributionState: toolUseContext.updateAttributionState,
2046|         }
2047|       : undefined
2048|     for (const [i, { hook }] of matchingHooks.entries()) {

executePreToolUseHooks

在工具执行 之前 调用。若 hasHookForEvent 为 false 直接 return,避免构建 hookInput。

构造 PreToolUseHookInput:spread createBaseHookInput,含 tool_name、tool_input、tool_use_id。

yield* executeHooks({ matchQuery: toolName, toolUseContext, requestPrompt, toolInputSummary })。

阻塞时 getPreToolHookBlockingMessage 格式化错误,query 层应停止 tool 执行。常见用途:lint、禁止写某些路径、录制审计日志。

源码引用: src/utils/hooks.ts · 第 3394–3436 行(共 5023 行)

3394| export async function* executePreToolHooks<ToolInput>(
3395|   toolName: string,
3396|   toolUseID: string,
3397|   toolInput: ToolInput,
3398|   toolUseContext: ToolUseContext,
3399|   permissionMode?: string,
3400|   signal?: AbortSignal,
3401|   timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
3402|   requestPrompt?: (
3403|     sourceName: string,
3404|     toolInputSummary?: string | null,
3405|   ) => (request: PromptRequest) => Promise<PromptResponse>,
3406|   toolInputSummary?: string | null,
3407| ): AsyncGenerator<AggregatedHookResult> {
3408|   const appState = toolUseContext.getAppState()
3409|   const sessionId = toolUseContext.agentId ?? getSessionId()
3410|   if (!hasHookForEvent('PreToolUse', appState, sessionId)) {
3411|     return
3412|   }
3413| 
3414|   logForDebugging(`executePreToolHooks called for tool: ${toolName}`, {
3415|     level: 'verbose',
3416|   })
3417| 
3418|   const hookInput: PreToolUseHookInput = {
3419|     ...createBaseHookInput(permissionMode, undefined, toolUseContext),
3420|     hook_event_name: 'PreToolUse',
3421|     tool_name: toolName,
3422|     tool_input: toolInput,
3423|     tool_use_id: toolUseID,
3424|   }
3425| 
3426|   yield* executeHooks({
3427|     hookInput,
3428|     toolUseID,
3429|     matchQuery: toolName,
3430|     signal,
3431|     timeoutMs,
3432|     toolUseContext,
3433|     requestPrompt,
3434|     toolInputSummary,
3435|   })
3436| }

executePostToolUse 与 Failure

PostToolUse:工具成功后,hookInput 含 tool_response,供用户脚本检查产出或发送通知。

PostToolUseFailure:工具抛错或返回错误时触发,matcher 仍为 tool_name,便于针对 Bash 失败运行诊断脚本。

两者都 yield* executeHooks,可能把 hook stdout 作为附加 user 消息反馈给模型(与 OutsideREPL 路径不同)。

源码引用: src/utils/hooks.ts · 第 3450–3525 行(共 5023 行)

3450| export async function* executePostToolHooks<ToolInput, ToolResponse>(
3451|   toolName: string,
3452|   toolUseID: string,
3453|   toolInput: ToolInput,
3454|   toolResponse: ToolResponse,
3455|   toolUseContext: ToolUseContext,
3456|   permissionMode?: string,
3457|   signal?: AbortSignal,
3458|   timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
3459| ): AsyncGenerator<AggregatedHookResult> {
3460|   const hookInput: PostToolUseHookInput = {
3461|     ...createBaseHookInput(permissionMode, undefined, toolUseContext),
3462|     hook_event_name: 'PostToolUse',
3463|     tool_name: toolName,
3464|     tool_input: toolInput,
3465|     tool_response: toolResponse,
3466|     tool_use_id: toolUseID,
3467|   }
3468| 
3469|   yield* executeHooks({
3470|     hookInput,
3471|     toolUseID,
3472|     matchQuery: toolName,
3473|     signal,
3474|     timeoutMs,
3475|     toolUseContext,
3476|   })
3477| }
3478| 
3479| /**
3480|  * Execute post-tool-use-failure hooks if configured
3481|  * @param toolName The name of the tool (e.g., 'Write', 'Edit', 'Bash')
3482|  * @param toolUseID The ID of the tool use
3483|  * @param toolInput The input that was passed to the tool
3484|  * @param error The error message from the failed tool call
3485|  * @param toolUseContext ToolUseContext for prompt-based hooks
3486|  * @param isInterrupt Whether the tool was interrupted by user
3487|  * @param permissionMode Optional permission mode from toolPermissionContext
3488|  * @param signal Optional AbortSignal to cancel hook execution
3489|  * @param timeoutMs Optional timeout in milliseconds for hook execution
3490|  * @returns Async generator that yields progress messages and blocking errors
3491|  */
3492| export async function* executePostToolUseFailureHooks<ToolInput>(
3493|   toolName: string,
3494|   toolUseID: string,
3495|   toolInput: ToolInput,
3496|   error: string,
3497|   toolUseContext: ToolUseContext,
3498|   isInterrupt?: boolean,
3499|   permissionMode?: string,
3500|   signal?: AbortSignal,
3501|   timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
3502| ): AsyncGenerator<AggregatedHookResult> {
3503|   const appState = toolUseContext.getAppState()
3504|   const sessionId = toolUseContext.agentId ?? getSessionId()
3505|   if (!hasHookForEvent('PostToolUseFailure', appState, sessionId)) {
3506|     return
3507|   }
3508| 
3509|   const hookInput: PostToolUseFailureHookInput = {
3510|     ...createBaseHookInput(permissionMode, undefined, toolUseContext),
3511|     hook_event_name: 'PostToolUseFailure',
3512|     tool_name: toolName,
3513|     tool_input: toolInput,
3514|     tool_use_id: toolUseID,
3515|     error,
3516|     is_interrupt: isInterrupt,
3517|   }
3518| 
3519|   yield* executeHooks({
3520|     hookInput,
3521|     toolUseID,
3522|     matchQuery: toolName,
3523|     signal,
3524|     timeoutMs,
3525|     toolUseContext,

executePermissionRequestHooks

当权限系统即将向用户弹窗(或 headless 需要 hook 决策)时触发。hookInput 含 permission_suggestions,可把 UI 上显示的规则建议传给脚本。

与 permissions.ts 内嵌的 PermissionRequest 处理互补:引擎侧先尝试 hook,再 fall through 到规则。

yield* executeHooks 参数与 PreToolUse 类似,支持 requestPrompt 供 prompt-based hook 向用户追问。

注意: 这与 hooks/useCanUseTool 的 React 弹窗并行存在——hook 可以 deny/allow,弹窗仍可显示,取决于返回决策与模式。

源码引用: src/utils/hooks.ts · 第 4157–4191 行(共 5023 行)

4157| export async function* executePermissionRequestHooks<ToolInput>(
4158|   toolName: string,
4159|   toolUseID: string,
4160|   toolInput: ToolInput,
4161|   toolUseContext: ToolUseContext,
4162|   permissionMode?: string,
4163|   permissionSuggestions?: PermissionUpdate[],
4164|   signal?: AbortSignal,
4165|   timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
4166|   requestPrompt?: (
4167|     sourceName: string,
4168|     toolInputSummary?: string | null,
4169|   ) => (request: PromptRequest) => Promise<PromptResponse>,
4170|   toolInputSummary?: string | null,
4171| ): AsyncGenerator<AggregatedHookResult> {
4172|   logForDebugging(`executePermissionRequestHooks called for tool: ${toolName}`)
4173| 
4174|   const hookInput: PermissionRequestHookInput = {
4175|     ...createBaseHookInput(permissionMode, undefined, toolUseContext),
4176|     hook_event_name: 'PermissionRequest',
4177|     tool_name: toolName,
4178|     tool_input: toolInput,
4179|     permission_suggestions: permissionSuggestions,
4180|   }
4181| 
4182|   yield* executeHooks({
4183|     hookInput,
4184|     toolUseID,
4185|     matchQuery: toolName,
4186|     signal,
4187|     timeoutMs,
4188|     toolUseContext,
4189|     requestPrompt,
4190|     toolInputSummary,
4191|   })

Stop、SessionEnd 与其它事件

executeStopHooks:回合结束,可阻塞继续(exit 2)。getStopHookMessage 格式化错误。

executeSessionEndHooks / executePreCompactHooks / executePostCompactHooks:走 executeHooksOutsideREPL,无 yield 给 Ink。

executeUserPromptSubmitHooks:用户提交 prompt 前,可完全 block 提交(getUserPromptSubmitHookBlockingMessage)。

executeNotificationHooks:系统通知,match notification_type。

读文件后部 export 列表可得到完整事件表;新增事件必须同时更新 getMatchingHooks switch 与 hooksConfigManager。

源码引用: src/utils/hooks.ts · 第 1894–1939 行(共 5023 行)

1894| export function getStopHookMessage(blockingError: HookBlockingError): string {
1895|   return `Stop hook feedback:\n${blockingError.blockingError}`
1896| }
1897| 
1898| /**
1899|  * Format a blocking error from a TeammateIdle hook.
1900|  * @param blockingError The blocking error from the hook
1901|  * @returns Formatted message to give feedback to the model
1902|  */
1903| export function getTeammateIdleHookMessage(
1904|   blockingError: HookBlockingError,
1905| ): string {
1906|   return `TeammateIdle hook feedback:\n${blockingError.blockingError}`
1907| }
1908| 
1909| /**
1910|  * Format a blocking error from a TaskCreated hook.
1911|  * @param blockingError The blocking error from the hook
1912|  * @returns Formatted message to give feedback to the model
1913|  */
1914| export function getTaskCreatedHookMessage(
1915|   blockingError: HookBlockingError,
1916| ): string {
1917|   return `TaskCreated hook feedback:\n${blockingError.blockingError}`
1918| }
1919| 
1920| /**
1921|  * Format a blocking error from a TaskCompleted hook.
1922|  * @param blockingError The blocking error from the hook
1923|  * @returns Formatted message to give feedback to the model
1924|  */
1925| export function getTaskCompletedHookMessage(
1926|   blockingError: HookBlockingError,
1927| ): string {
1928|   return `TaskCompleted hook feedback:\n${blockingError.blockingError}`
1929| }
1930| 
1931| /**
1932|  * Format a list of blocking errors from a UserPromptSubmit hook's configured commands.
1933|  * @param blockingErrors Array of blocking errors from hooks
1934|  * @returns Formatted blocking message
1935|  */
1936| export function getUserPromptSubmitHookBlockingMessage(
1937|   blockingError: HookBlockingError,
1938| ): string {
1939|   return `UserPromptSubmit operation blocked by hook:\n${blockingError.blockingError}`

源码引用: src/utils/hooks.ts · 第 3826–3865 行(共 5023 行)

3826| export async function* executeUserPromptSubmitHooks(
3827|   prompt: string,
3828|   permissionMode: string,
3829|   toolUseContext: ToolUseContext,
3830|   requestPrompt?: (
3831|     sourceName: string,
3832|     toolInputSummary?: string | null,
3833|   ) => (request: PromptRequest) => Promise<PromptResponse>,
3834| ): AsyncGenerator<AggregatedHookResult> {
3835|   const appState = toolUseContext.getAppState()
3836|   const sessionId = toolUseContext.agentId ?? getSessionId()
3837|   if (!hasHookForEvent('UserPromptSubmit', appState, sessionId)) {
3838|     return
3839|   }
3840| 
3841|   const hookInput: UserPromptSubmitHookInput = {
3842|     ...createBaseHookInput(permissionMode),
3843|     hook_event_name: 'UserPromptSubmit',
3844|     prompt,
3845|   }
3846| 
3847|   yield* executeHooks({
3848|     hookInput,
3849|     toolUseID: randomUUID(),
3850|     signal: toolUseContext.abortController.signal,
3851|     timeoutMs: TOOL_HOOK_EXECUTION_TIMEOUT_MS,
3852|     toolUseContext,
3853|     requestPrompt,
3854|   })
3855| }
3856| 
3857| /**
3858|  * Execute session start hooks if configured
3859|  * @param source The source of the session start (startup, resume, clear)
3860|  * @param sessionId Optional The session id to use as hook input
3861|  * @param agentType Optional The agent type (from --agent flag) running this session
3862|  * @param model Optional The model being used for this session
3863|  * @param signal Optional AbortSignal to cancel hook execution
3864|  * @param timeoutMs Optional timeout in milliseconds for hook execution
3865|  * @returns Async generator that yields progress messages and hook results

源码目录

子目录 utils/hooks/ 含 hooksConfigManager、类型定义与测试辅助。改 matcher 规则时 必须 双文件对照注释。

动手练习

  1. 在项目 .claude/settings.json 添加 PreToolUse echo 命令,观察 verbose 日志中 matchQuery
  2. 未接受 trust 时运行,确认 executeHooks 跳过日志
  3. 制造 exit 2 阻塞,确认 getPreToolHookBlockingMessage 文案出现在 REPL
  4. 对比 permissions 章 headless PermissionRequest 与 executePermissionRequestHooks 调用顺序

本章小结与延伸

utils/hooks.ts = 用户自动化与审计的总线。改 matcher 逻辑需同步 hooksConfigManager.ts。 继续学习:

  • permissions
  • hooks 模块(React)
Prev
permissions · 工具权限决策
Next
REPL · 主屏编排