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

本章总览

useCanUseTool 是 src/hooks/ 中架构权重最高的 Hook:它把引擎层 hasPermissionsToUseTool 的决策结果,转换成 REPL 可消费的 Promise,并在需要用户确认时驱动 PermissionRequest 弹窗队列。本章带源码走读,要求你能从 query 循环一直追到弹窗 UI。

学完本章你应该能

  • 解释 CanUseToolFn 类型与各参数在 tool_use 时的含义
  • 画出 allow / deny / ask 三分支及 ask 时的入队逻辑
  • 说明 toolPermission/handlers 三种模式的适用场景
  • 能在源码树中定位 PermissionContext 与 interactiveHandler

核心概念(先读懂这些)

为什么是 Promise 而不是同步返回

query.ts 在每次 tool_use 前 await canUseTool()。交互式会话中用户点击 Allow 可能数秒后才发生,Ink 仍需刷新 spinner。Promise 把「决策未完成」与「UI 线程」解耦:Hook 内部入队 confirm 项,用户操作后 resolve。Headless/SDK 模式若策略已 allow,则快速 resolve,不弹窗。

PermissionContext 解耦 React

createPermissionContext 把 tool、input、toolUseID 等封装为 ctx,并提供 buildAllow/buildDeny、resolveIfAborted。PermissionQueueOps 抽象 push/remove/update,REPL 用 React state 实现,测试可 mock。这是「引擎逻辑不依赖 React,UI 适配层依赖 React」的典型分层。

建议学习步骤

  1. 阅读下方源码块 A(CanUseToolFn 类型与 Hook 入口)
  2. 阅读源码块 B(allow 分支与 classifier 联动)
  3. 打开 ask 分支,对照 PermissionContext 源码块 C
  4. 在源码树中点击 toolPermission/ 子目录核对文件清单

常见误区

注意

React Compiler 生成的 _c 缓存语法可忽略,聚焦 t0 回调内的业务分支

注意

不要把 executePermissionRequestHooks(utils/hooks.ts)与本 Hook 混淆

在架构中的调用位置

当 Claude 返回 tool_use block,query 循环在调用 StreamingToolExecutor 之前执行:

query.ts
  → canUseTool(tool, input, toolUseContext, assistantMessage, toolUseID)
       ↓ REPL 注册:useCanUseTool(...) 的返回值
  → PermissionDecision { behavior: allow | deny | ask }
  → 若 allow:进入工具执行
  → 若 deny:生成 tool_result 错误
  → 若 ask:挂起直至用户确认

REPL.tsx 在初始化 query 时传入 canUseTool 函数引用。改权限 UI 行为应优先改 useCanUseTool 或其 handlers,而非直接改 query.ts。

CanUseToolFn 类型与 Hook 入口

下列源码来自反编译 v2.1.88。类型定义明确 forceDecision 可选参数,用于测试注入或 replay:

阅读要点:

  1. 导入链横跨 Tool、permissions、analytics、toolPermission 子目录,说明这是「横切接缝」
  2. useCallback 包裹的 t0 即实际 canUseTool 实现
  3. 第一行即 createPermissionContext,后续分支都通过 ctx 操作

源码引用: src/hooks/useCanUseTool.tsx · 第 27–38 行(共 355 行)

  27|   setYoloClassifierApproval,
  28| } from '../utils/classifierApprovals.js'
  29| import { logForDebugging } from '../utils/debug.js'
  30| import { AbortError } from '../utils/errors.js'
  31| import { logError } from '../utils/log.js'
  32| import type { PermissionDecision } from '../utils/permissions/PermissionResult.js'
  33| import { hasPermissionsToUseTool } from '../utils/permissions/permissions.js'
  34| import { jsonStringify } from '../utils/slowOperations.js'
  35| import { handleCoordinatorPermission } from './toolPermission/handlers/coordinatorHandler.js'
  36| import { handleInteractivePermission } from './toolPermission/handlers/interactiveHandler.js'
  37| import { handleSwarmWorkerPermission } from './toolPermission/handlers/swarmWorkerHandler.js'
  38| import {

allow 分支:配置放行与 classifier

当 hasPermissionsToUseTool 返回 behavior === "allow" 时,路径最短:

  • 检查 abort 信号,避免 turn 已取消仍 resolve
  • TRANSCRIPT_CLASSIFIER feature 开启时,auto-mode classifier 放行会写入 setYoloClassifierApproval
  • ctx.logDecision 记录 analytics,source 为 "config"
  • resolve(ctx.buildAllow(...)) 把可能更新的 input 一并返回

工程含义: 大部分只读工具、已在 allowlist 中的 Bash 命令走此分支,无 UI 打扰。

源码引用: src/hooks/useCanUseTool.tsx · 第 39–54 行(共 355 行)

  39|   createPermissionContext,
  40|   createPermissionQueueOps,
  41| } from './toolPermission/PermissionContext.js'
  42| import { logPermissionDecision } from './toolPermission/permissionLogging.js'
  43| 
  44| export type CanUseToolFn<
  45|   Input extends Record<string, unknown> = Record<string, unknown>,
  46| > = (
  47|   tool: ToolType,
  48|   input: Input,
  49|   toolUseContext: ToolUseContext,
  50|   assistantMessage: AssistantMessage,
  51|   toolUseID: string,
  52|   forceDecision?: PermissionDecision<Input>,
  53| ) => Promise<PermissionDecision<Input>>
  54| 

deny 分支:自动模式拒绝与通知

deny 时除 resolve(result) 外,auto-mode classifier 拒绝会:

  1. recordAutoModeDenial 持久化拒绝记录
  2. addNotification 在 REPL 顶部展示 error 色横幅,并提示 /permissions

这体现「拒绝不仅是静默失败,还要给用户可操作的反馈」。

源码引用: src/hooks/useCanUseTool.tsx · 第 64–92 行(共 355 行)

  64|       input,
  65|       toolUseContext,
  66|       assistantMessage,
  67|       toolUseID,
  68|       forceDecision,
  69|     ) => {
  70|       return new Promise(resolve => {
  71|         const ctx = createPermissionContext(
  72|           tool,
  73|           input,
  74|           toolUseContext,
  75|           assistantMessage,
  76|           toolUseID,
  77|           setToolPermissionContext,
  78|           createPermissionQueueOps(setToolUseConfirmQueue),
  79|         )
  80| 
  81|         if (ctx.resolveIfAborted(resolve)) return
  82| 
  83|         const decisionPromise =
  84|           forceDecision !== undefined
  85|             ? Promise.resolve(forceDecision)
  86|             : hasPermissionsToUseTool(
  87|                 tool,
  88|                 input,
  89|                 toolUseContext,
  90|                 assistantMessage,
  91|                 toolUseID,
  92|               )

ask 分支与 Coordinator

behavior === "ask" 进入交互式权限流程。若 toolPermissionContext.awaitAutomatedChecksBeforeDialog 为真,先 await handleCoordinatorPermission(多 Agent 协调场景,可能等待 Bash classifier)。

随后通常进入 handleInteractivePermission(普通 REPL)或 handleSwarmWorkerPermission(Swarm 工人)。阅读建议: 在源码树展开 toolPermission/handlers/,三个 handler 文件对照本段文字。

PermissionContext 负责统一:队列操作、abort 检测、buildAllow/buildDeny、执行 permission request hooks(此处 hooks 指 utils/hooks.ts 的 PreToolUse 类钩子,与目录名 hooks/ 不同)。

源码引用: src/hooks/useCanUseTool.tsx · 第 93–110 行(共 355 行)

  93| 
  94|         return decisionPromise
  95|           .then(async result => {
  96|             // [ANT-ONLY] Log all tool permission decisions with tool name and args
  97|             if ("external" === 'ant') {
  98|               logEvent('tengu_internal_tool_permission_decision', {
  99|                 toolName: sanitizeToolNameForAnalytics(tool.name),
 100|                 behavior:
 101|                   result.behavior as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
 102|                 // Note: input contains code/filepaths, only log for ants
 103|                 input: jsonStringify(
 104|                   input,
 105|                 ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
 106|                 messageID:
 107|                   ctx.messageId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
 108|                 isMcp: tool.isMcp ?? false,
 109|               })
 110|             }

源码引用: src/hooks/toolPermission/PermissionContext.ts · 第 45–75 行(共 389 行)

  45| type PermissionApprovalSource =
  46|   | { type: 'hook'; permanent?: boolean }
  47|   | { type: 'user'; permanent: boolean }
  48|   | { type: 'classifier' }
  49| 
  50| type PermissionRejectionSource =
  51|   | { type: 'hook' }
  52|   | { type: 'user_abort' }
  53|   | { type: 'user_reject'; hasFeedback: boolean }
  54| 
  55| // Generic interface for permission queue operations, decoupled from React.
  56| // In the REPL, these are backed by React state.
  57| type PermissionQueueOps = {
  58|   push(item: ToolUseConfirm): void
  59|   remove(toolUseID: string): void
  60|   update(toolUseID: string, patch: Partial<ToolUseConfirm>): void
  61| }
  62| 
  63| type ResolveOnce<T> = {
  64|   resolve(value: T): void
  65|   isResolved(): boolean
  66|   /**
  67|    * Atomically check-and-mark as resolved. Returns true if this caller
  68|    * won the race (nobody else has resolved yet), false otherwise.
  69|    * Use this in async callbacks BEFORE awaiting, to close the window
  70|    * between the `isResolved()` check and the actual `resolve()` call.
  71|    */
  72|   claim(): boolean
  73| }
  74| 
  75| function createResolveOnce<T>(resolve: (value: T) => void): ResolveOnce<T> {

PermissionQueueOps:与 React 状态的边界

PermissionContext.ts 开头定义 PermissionQueueOps 接口——纯数据结构,不 import React:

push(item)   → 新的 PermissionRequest 入队
remove(id)   → 用户决策后移除
update(id)   → 更新 confirm 状态(如加载中)

REPL 把 setToolUseConfirmQueue 包一层传给 createPermissionQueueOps。这样 query 引擎只看见 Promise,不看见 React setState。

源码引用: src/hooks/toolPermission/PermissionContext.ts · 第 55–90 行(共 389 行)

  55| // Generic interface for permission queue operations, decoupled from React.
  56| // In the REPL, these are backed by React state.
  57| type PermissionQueueOps = {
  58|   push(item: ToolUseConfirm): void
  59|   remove(toolUseID: string): void
  60|   update(toolUseID: string, patch: Partial<ToolUseConfirm>): void
  61| }
  62| 
  63| type ResolveOnce<T> = {
  64|   resolve(value: T): void
  65|   isResolved(): boolean
  66|   /**
  67|    * Atomically check-and-mark as resolved. Returns true if this caller
  68|    * won the race (nobody else has resolved yet), false otherwise.
  69|    * Use this in async callbacks BEFORE awaiting, to close the window
  70|    * between the `isResolved()` check and the actual `resolve()` call.
  71|    */
  72|   claim(): boolean
  73| }
  74| 
  75| function createResolveOnce<T>(resolve: (value: T) => void): ResolveOnce<T> {
  76|   let claimed = false
  77|   let delivered = false
  78|   return {
  79|     resolve(value: T) {
  80|       if (delivered) return
  81|       delivered = true
  82|       claimed = true
  83|       resolve(value)
  84|     },
  85|     isResolved() {
  86|       return claimed
  87|     },
  88|     claim() {
  89|       if (claimed) return false
  90|       claimed = true

ask 分支深度:Coordinator、Swarm 与 Bash 投机分类

behavior === "ask" 时,useCanUseTool 并非直接弹窗,而是按 环境标志 依次尝试短路:

1. Coordinator 路径 若 awaitAutomatedChecksBeforeDialog 为真,先 await handleCoordinatorPermission。多 Agent 协调场景可能等待 Bash classifier 或远程决策;若返回 decision 则直接 resolve,不再进入 REPL 弹窗。

2. Swarm Worker 路径handleSwarmWorkerPermission 处理工人 Agent 权限——主 REPL 不弹窗时由 worker 侧规则消化。

3. Bash 投机 classifier(BASH_CLASSIFIER feature) 对 Bash 工具且存在 pendingClassifierCheck 时,peekSpeculativeClassifierCheck 与 2s timeout race。高置信度 allow 则 consume 并 resolve allow,跳过弹窗——这是「用户还在看描述时后台已跑完 classifier」的优化。

4. 默认 interactive 以上皆未 resolve 时,handleInteractivePermission({ ctx, description, result, bridgeCallbacks, channelCallbacks }, resolve) 入队 ToolUseConfirm。

阅读 ask 分支时应对照 feature gate:TRANSCRIPT_CLASSIFIER、BASH_CLASSIFIER、BRIDGE_MODE、KAIROS_CHANNELS 会显著改变路径。

源码引用: src/hooks/useCanUseTool.tsx · 第 93–168 行(共 355 行)

  93| 
  94|         return decisionPromise
  95|           .then(async result => {
  96|             // [ANT-ONLY] Log all tool permission decisions with tool name and args
  97|             if ("external" === 'ant') {
  98|               logEvent('tengu_internal_tool_permission_decision', {
  99|                 toolName: sanitizeToolNameForAnalytics(tool.name),
 100|                 behavior:
 101|                   result.behavior as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
 102|                 // Note: input contains code/filepaths, only log for ants
 103|                 input: jsonStringify(
 104|                   input,
 105|                 ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
 106|                 messageID:
 107|                   ctx.messageId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
 108|                 isMcp: tool.isMcp ?? false,
 109|               })
 110|             }
 111| 
 112|             // Has permissions to use tool, granted in config
 113|             if (result.behavior === 'allow') {
 114|               if (ctx.resolveIfAborted(resolve)) return
 115|               // Track auto mode classifier approvals for UI display
 116|               if (
 117|                 feature('TRANSCRIPT_CLASSIFIER') &&
 118|                 result.decisionReason?.type === 'classifier' &&
 119|                 result.decisionReason.classifier === 'auto-mode'
 120|               ) {
 121|                 setYoloClassifierApproval(
 122|                   toolUseID,
 123|                   result.decisionReason.reason,
 124|                 )
 125|               }
 126| 
 127|               ctx.logDecision({ decision: 'accept', source: 'config' })
 128| 
 129|               resolve(
 130|                 ctx.buildAllow(result.updatedInput ?? input, {
 131|                   decisionReason: result.decisionReason,
 132|                 }),
 133|               )
 134|               return
 135|             }
 136| 
 137|             const appState = toolUseContext.getAppState()
 138|             const description = await tool.description(input as never, {
 139|               isNonInteractiveSession:
 140|                 toolUseContext.options.isNonInteractiveSession,
 141|               toolPermissionContext: appState.toolPermissionContext,
 142|               tools: toolUseContext.options.tools,
 143|             })
 144| 
 145|             if (ctx.resolveIfAborted(resolve)) return
 146| 
 147|             // Does not have permissions to use tool, check the behavior
 148|             switch (result.behavior) {
 149|               case 'deny': {
 150|                 logPermissionDecision(
 151|                   {
 152|                     tool,
 153|                     input,
 154|                     toolUseContext,
 155|                     messageId: ctx.messageId,
 156|                     toolUseID,
 157|                   },
 158|                   { decision: 'reject', source: 'config' },
 159|                 )
 160|                 if (
 161|                   feature('TRANSCRIPT_CLASSIFIER') &&
 162|                   result.decisionReason?.type === 'classifier' &&
 163|                   result.decisionReason.classifier === 'auto-mode'
 164|                 ) {
 165|                   recordAutoModeDenial({
 166|                     toolName: tool.name,
 167|                     display: description,
 168|                     reason: result.decisionReason.reason ?? '',

handleInteractivePermission:队列、race 与 resolve-once

interactiveHandler.ts 是 ask 分支的「重逻辑」所在。函数 不返回 Promise,而是通过 push ToolUseConfirm 并在 onAllow/onReject 回调里 resolve 外层 Promise。

核心机制:

  • createResolveOnce(resolve) — 保证只 resolve 一次,避免用户点击 Allow 与 classifier 同时返回双 resolve
  • userInteracted 标志 — classifier/hook 后台完成时,若用户已操作则忽略自动结果
  • 异步并行:permission request hooks(utils/hooks.ts PreToolUse)、Bash classifier、bridge/channel 远程确认与 UI 交互 race
  • onUserInteraction — 用户首次聚焦弹窗时标记,影响 classifier 是否仍可自动放行

Bridge / Channel 扩展:

  • BRIDGE_MODE 传入 replBridgePermissionCallbacks,向远端 UI 转发权限请求
  • KAIROS_CHANNELS 传入 channelPermissionCallbacks,支持手机端 yes/no 回复

工程调试: 弹窗不出现但 turn 挂起 → 查 confirm queue 是否 push;弹窗闪退 → 查 resolveOnce 是否被 classifier 抢先;Bash 无弹窗直接 allow → 查 speculative classifier race 日志。

源码引用: src/hooks/toolPermission/handlers/interactiveHandler.ts · 第 43–80 行(共 537 行)

  43| /**
  44|  * Handles the interactive (main-agent) permission flow.
  45|  *
  46|  * Pushes a ToolUseConfirm entry to the confirm queue with callbacks:
  47|  * onAbort, onAllow, onReject, recheckPermission, onUserInteraction.
  48|  *
  49|  * Runs permission hooks and bash classifier checks asynchronously in the
  50|  * background, racing them against user interaction. Uses a resolve-once
  51|  * guard and `userInteracted` flag to prevent multiple resolutions.
  52|  *
  53|  * This function does NOT return a Promise -- it sets up callbacks that
  54|  * eventually call `resolve()` to resolve the outer promise owned by
  55|  * the caller.
  56|  */
  57| function handleInteractivePermission(
  58|   params: InteractivePermissionParams,
  59|   resolve: (decision: PermissionDecision) => void,
  60| ): void {
  61|   const {
  62|     ctx,
  63|     description,
  64|     result,
  65|     awaitAutomatedChecksBeforeDialog,
  66|     bridgeCallbacks,
  67|     channelCallbacks,
  68|   } = params
  69| 
  70|   const { resolve: resolveOnce, isResolved, claim } = createResolveOnce(resolve)
  71|   let userInteracted = false
  72|   let checkmarkTransitionTimer: ReturnType<typeof setTimeout> | undefined
  73|   // Hoisted so onDismissCheckmark (Esc during checkmark window) can also
  74|   // remove the abort listener — not just the timer callback.
  75|   let checkmarkAbortHandler: (() => void) | undefined
  76|   const bridgeRequestId = bridgeCallbacks ? randomUUID() : undefined
  77|   // Hoisted so local/hook/classifier wins can remove the pending channel
  78|   // entry. No "tell remote to dismiss" equivalent — the text sits in your
  79|   // phone, and a stale "yes abc123" after local-resolve falls through
  80|   // tryConsumeReply (entry gone) and gets enqueued as normal chat.

forceDecision、abort 与 finally 清理

CanUseToolFn 第六参数 forceDecision 用于测试或 replay:传入时跳过 hasPermissionsToUseTool,直接走 allow/deny/ask 分支逻辑。SDK 集成测试可注入固定决策而不改 permissions 文件。

Abort 处理:

  • 每个分支入口调用 ctx.resolveIfAborted(resolve) — turn 已取消则 early resolve
  • .catch 捕获 AbortError / APIUserAbortError → logForDebugging + ctx.cancelAndAbort
  • 其他错误 → logError + cancelAndAbort

.finally 始终 clearClassifierChecking(toolUseID),避免 spinner 状态泄漏到下一 tool_use。

deny 分支的 addNotification(auto-mode-denied key,immediate priority)与 notifs 章 immediate 语义一致——权限拒绝需要立刻打断当前横幅。

源码引用: src/hooks/useCanUseTool.tsx · 第 27–37 行(共 355 行)

  27|   setYoloClassifierApproval,
  28| } from '../utils/classifierApprovals.js'
  29| import { logForDebugging } from '../utils/debug.js'
  30| import { AbortError } from '../utils/errors.js'
  31| import { logError } from '../utils/log.js'
  32| import type { PermissionDecision } from '../utils/permissions/PermissionResult.js'
  33| import { hasPermissionsToUseTool } from '../utils/permissions/permissions.js'
  34| import { jsonStringify } from '../utils/slowOperations.js'
  35| import { handleCoordinatorPermission } from './toolPermission/handlers/coordinatorHandler.js'
  36| import { handleInteractivePermission } from './toolPermission/handlers/interactiveHandler.js'
  37| import { handleSwarmWorkerPermission } from './toolPermission/handlers/swarmWorkerHandler.js'

源码引用: src/hooks/useCanUseTool.tsx · 第 171–182 行(共 355 行)

 171|                   toolUseContext.addNotification?.({
 172|                     key: 'auto-mode-denied',
 173|                     priority: 'immediate',
 174|                     jsx: (
 175|                       <>
 176|                         <Text color="error">
 177|                           {tool.userFacingName(input).toLowerCase()} denied by
 178|                           auto mode
 179|                         </Text>
 180|                         <Text dimColor> · /permissions</Text>
 181|                       </>
 182|                     ),

与 utils/permissions 的协作

hasPermissionsToUseTool 在 utils/permissions/permissions.ts 中实现,读取:

  • toolPermissionContext(会话级 allow/deny 规则)
  • 工具自身 permissionType
  • plan 模式 / acceptEdits 等标志

useCanUseTool 不做 规则求值,只做「把结果翻译成 UI 或 Promise」。调试权限问题时:

  1. 先在 permissions.ts 看为何返回 ask
  2. 再在 useCanUseTool 看 ask 是否正确入队
  3. 最后在 components/permissions/PermissionRequest 看渲染

这条三步链是 hooks 模块最重要的实战技能。

源码目录(本主题相关文件)

点击 toolPermission/ 可展开查看 handlers、permissionLogging 等关联文件。

动手练习

  1. 在终端触发一次需确认的 FileEdit,观察 PermissionRequest 出现时机与 toolUseID
  2. 对照源码块 B/C,在纸上画出 allow/deny/ask 三分支
  3. 修改 settings 中 permissions 为 deny 某工具,确认走 deny 分支且出现通知(若适用)
  4. 在源码树点击 PermissionContext.ts,跳转到对应源码块 C

本章小结与延伸

useCanUseTool = 权限决策的 UI 适配器。下一章建议读「权限与安全」专题或 mod-components 中的 PermissionRequest 组件。 继续学习:

  • hooks 模块总览
  • 权限与安全
Prev
模块: hooks
Next
输入与快捷键 Hook