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

本章总览

tools/BashTool/(18 个文件)实现 Claude Code 最高频工具:在 user shell 中执行命令,贯穿权限 AST 解析、沙箱、只读判定、sed 模拟编辑、后台任务与 transcript 折叠显示。BashTool.tsx 通过 buildTool 导出 Bash;bashPermissions.ts(2600+ 行)实现 bashToolHasPermission 与分类器集成。本章要求你能从一条 Bash tool_use 追踪到 exec 调用、权限 deny 原因、以及 stdout 如何变成 model-facing tool_result。

学完本章你应该能

  • 解释 isReadOnly / isConcurrencySafe 与 checkReadOnlyConstraints 的关系
  • 说明 bashToolHasPermission 与 permissions.ts 规则引擎的分工
  • 理解 isSearchOrReadBashCommand 如何驱动 UI 折叠
  • 掌握 run_in_background、沙箱、sed 模拟编辑三条特殊路径
  • 能在 StreamingToolExecutor 中定位 Bash 错误级联 sibling abort 逻辑

核心概念(先读懂这些)

Bash 权限是两层叠加

permissions.ts 的 deny/ask 规则先匹配工具名 Bash(pattern);bashToolHasPermission 再解析命令 AST,检查路径、sed、operator、沙箱 auto-allow 等。分类器(auto 模式)在 bashPermissions 内启动 speculative check。Hook 的 Bash(git *) 模式依赖 preparePermissionMatcher 拆分子命令。

只读命令可并发

isConcurrencySafe 委托 isReadOnly:通过 checkReadOnlyConstraints 的 compound 命令分析,纯 cat/grep/find 管道可与其他只读 Bash 并行。写操作或含 cd 的 compound 串行。StreamingToolExecutor 据此调度;Bash 错误会 abort siblingAbortController 取消并行 Bash。

模型 payload 与 UI 分离

mapToolResultToToolResultBlockParam 可注入 persisted-output 包装、backgroundInfo;renderToolResultMessage / extractSearchText 使用原始 stdout。注释强调 UI never 见 persistedOutputPath wrapper,避免 transcript 与模型上下文不一致。

建议学习步骤

  1. 阅读 buildTool 导出块:权限与并发钩子
  2. 阅读 isSearchOrReadBashCommand 折叠逻辑
  3. 阅读 call() 入口与 runShellCommand 生成器
  4. 阅读 bashToolHasPermission 入口
  5. 阅读 preparePermissionMatcher 与 compound 命令
  6. 对照 UI.tsx 渲染与 extractSearchText

常见误区

注意

userFacingName 内避免重 parse(#21605 shimmer 死循环);沙箱指示器用 env 门控

注意

sed 模拟编辑 _simulatedSedEdit 路径绕过 shell,直接写文件

注意

BASH_TOOL_NAME 常量勿与权限规则硬编码字符串分叉

目录结构与职责

BashTool/ 核心文件:

文件职责
BashTool.tsxbuildTool 主实现:call、权限钩子、结果映射
bashPermissions.tsbashToolHasPermission、分类器、规则匹配
bashSecurity.ts命令安全启发式(legacy)
readOnlyValidation.ts只读 compound 分析
shouldUseSandbox.ts沙箱决策
sedEditParser.ts / sedValidation.tssed -i 模拟为 FileEdit
UI.tsxrenderToolUseMessage、进度、结果折叠
prompt.ts工具 description 与 timeout 常量
toolName.tsBASH_TOOL_NAME = 'Bash'

Bash 是 strict: true 工具,且 toAutoClassifierInput 直接返回 command 字符串供 auto 模式分类器消费。

buildTool 导出:并发与权限钩子

BashTool 在 buildTool({...}) 里把「能不能并行」和「能不能执行」拆成两条链路。并发侧,isReadOnly 调用 checkReadOnlyConstraints,对 compound 命令做 AST/规则分析:纯 grep|cat|ls 管道可并行,含写 redirect、cd 出项目根、或 destructive 子命令则只读失败。执行权限侧,checkPermissions 指向 bashToolHasPermission,它会叠加权限规则、路径约束、沙箱、分类器与 hook matcher。

关键配置:

  • maxResultSizeChars: 30_000 — 超出则 toolResultStorage 落盘
  • isConcurrencySafe → isReadOnly(input)
  • checkPermissions → bashToolHasPermission
  • preparePermissionMatcher — 拆 compound 命令匹配 hook 模式
  • isSearchOrReadCommand — 委托 isSearchOrReadBashCommand
  • validateInput — MONITOR_TOOL 下拦截 sleep 模式

preparePermissionMatcher 会把 ls && git push 拆成子命令,避免 hook 模式 Bash(git *) 被 compound 绕过。parse 失败时 fail-safe 返回 () => true,保证安全 hook 不会因为解析器异常而失效。

源码引用: src/tools/BashTool/BashTool.tsx · 第 640–720 行(共 1473 行)

 640| export const BashTool = buildTool({
 641|   name: BASH_TOOL_NAME,
 642|   searchHint: 'execute shell commands',
 643|   // 30K chars - tool result persistence threshold
 644|   maxResultSizeChars: 30_000,
 645|   strict: true,
 646|   async description({ description }) {
 647|     return description || 'Run shell command'
 648|   },
 649|   async prompt() {
 650|     return getSimplePrompt()
 651|   },
 652|   isConcurrencySafe(input) {
 653|     return this.isReadOnly?.(input) ?? false
 654|   },
 655|   isReadOnly(input) {
 656|     const compoundCommandHasCd = commandHasAnyCd(input.command)
 657|     const result = checkReadOnlyConstraints(input, compoundCommandHasCd)
 658|     return result.behavior === 'allow'
 659|   },
 660|   toAutoClassifierInput(input) {
 661|     return input.command
 662|   },
 663|   async preparePermissionMatcher({ command }) {
 664|     // Hook `if` filtering is "no match → skip hook" (deny-like semantics), so
 665|     // compound commands must fire the hook if ANY subcommand matches. Without
 666|     // splitting, `ls && git push` would bypass a `Bash(git *)` security hook.
 667|     const parsed = await parseForSecurity(command)
 668|     if (parsed.kind !== 'simple') {
 669|       // parse-unavailable / too-complex: fail safe by running the hook.
 670|       return () => true
 671|     }
 672|     // Match on argv (strips leading VAR=val) so `FOO=bar git push` still
 673|     // matches `Bash(git *)`.
 674|     const subcommands = parsed.commands.map(c => c.argv.join(' '))
 675|     return pattern => {
 676|       const prefix = permissionRuleExtractPrefix(pattern)
 677|       return subcommands.some(cmd => {
 678|         if (prefix !== null) {
 679|           return cmd === prefix || cmd.startsWith(`${prefix} `)
 680|         }
 681|         return matchWildcardPattern(pattern, cmd)
 682|       })
 683|     }
 684|   },
 685|   isSearchOrReadCommand(input) {
 686|     const parsed = inputSchema().safeParse(input)
 687|     if (!parsed.success)
 688|       return { isSearch: false, isRead: false, isList: false }
 689|     return isSearchOrReadBashCommand(parsed.data.command)
 690|   },
 691|   get inputSchema(): InputSchema {
 692|     return inputSchema()
 693|   },
 694|   get outputSchema(): OutputSchema {
 695|     return outputSchema()
 696|   },
 697|   userFacingName(input) {
 698|     if (!input) {
 699|       return 'Bash'
 700|     }
 701|     // Render sed in-place edits as file edits
 702|     if (input.command) {
 703|       const sedInfo = parseSedEditCommand(input.command)
 704|       if (sedInfo) {
 705|         return fileEditUserFacingName({
 706|           file_path: sedInfo.filePath,
 707|           old_string: 'x',
 708|         })
 709|       }
 710|     }
 711|     // Env var FIRST: shouldUseSandbox → splitCommand_DEPRECATED → shell-quote's
 712|     // `new RegExp` per call. userFacingName runs per-render for every bash
 713|     // message in history; with ~50 msgs + one slow-to-tokenize command, this
 714|     // exceeds the shimmer tick → transition abort → infinite retry (#21605).
 715|     return isEnvTruthy(process.env.CLAUDE_CODE_BASH_SANDBOX_SHOW_INDICATOR) &&
 716|       shouldUseSandbox(input)
 717|       ? 'SandboxedBash'
 718|       : 'Bash'
 719|   },
 720|   getToolUseSummary(input) {

源码引用: src/tools/BashTool/BashTool.tsx · 第 198–267 行(共 1473 行)

 198| export function isSearchOrReadBashCommand(command: string): {
 199|   isSearch: boolean
 200|   isRead: boolean
 201|   isList: boolean
 202| } {
 203|   let partsWithOperators: string[]
 204|   try {
 205|     partsWithOperators = splitCommandWithOperators(command)
 206|   } catch {
 207|     // If we can't parse the command due to malformed syntax,
 208|     // it's not a search/read command
 209|     return { isSearch: false, isRead: false, isList: false }
 210|   }
 211| 
 212|   if (partsWithOperators.length === 0) {
 213|     return { isSearch: false, isRead: false, isList: false }
 214|   }
 215| 
 216|   let hasSearch = false
 217|   let hasRead = false
 218|   let hasList = false
 219|   let hasNonNeutralCommand = false
 220|   let skipNextAsRedirectTarget = false
 221| 
 222|   for (const part of partsWithOperators) {
 223|     if (skipNextAsRedirectTarget) {
 224|       skipNextAsRedirectTarget = false
 225|       continue
 226|     }
 227| 
 228|     if (part === '>' || part === '>>' || part === '>&') {
 229|       skipNextAsRedirectTarget = true
 230|       continue
 231|     }
 232| 
 233|     if (part === '||' || part === '&&' || part === '|' || part === ';') {
 234|       continue
 235|     }
 236| 
 237|     const baseCommand = part.trim().split(/\s+/)[0]
 238|     if (!baseCommand) {
 239|       continue
 240|     }
 241| 
 242|     if (BASH_SEMANTIC_NEUTRAL_COMMANDS.has(baseCommand)) {
 243|       continue
 244|     }
 245| 
 246|     hasNonNeutralCommand = true
 247| 
 248|     const isPartSearch = BASH_SEARCH_COMMANDS.has(baseCommand)
 249|     const isPartRead = BASH_READ_COMMANDS.has(baseCommand)
 250|     const isPartList = BASH_LIST_COMMANDS.has(baseCommand)
 251| 
 252|     if (!isPartSearch && !isPartRead && !isPartList) {
 253|       return { isSearch: false, isRead: false, isList: false }
 254|     }
 255| 
 256|     if (isPartSearch) hasSearch = true
 257|     if (isPartRead) hasRead = true
 258|     if (isPartList) hasList = true
 259|   }
 260| 
 261|   // Only neutral commands (e.g., just "echo foo") -- not collapsible
 262|   if (!hasNonNeutralCommand) {
 263|     return { isSearch: false, isRead: false, isList: false }
 264|   }
 265| 
 266|   return { isSearch: hasSearch, isRead: hasRead, isList: hasList }
 267| }

isSearchOrReadBashCommand:UI 折叠

折叠显示(condensed transcript)依赖 isSearchOrReadBashCommand:

命令集合:

  • BASH_SEARCH_COMMANDS — find、grep、rg 等
  • BASH_READ_COMMANDS — cat、head、jq、awk 等
  • BASH_LIST_COMMANDS — ls、tree、du(单独 isList 语义)
  • BASH_SEMANTIC_NEUTRAL_COMMANDS — echo、true 等可跳过

规则: splitCommandWithOperators 解析失败 → 不折叠。pipeline 中任一部分非 search/read/list → 整体不折叠。仅 neutral 命令(如单独 echo)也不折叠。

这与 Tool 接口 isSearchOrReadCommand 挂钩,Message 组件据此合并多行 Bash 输出,减少 transcript 噪音。

源码引用: src/tools/BashTool/BashTool.tsx · 第 59–78 行(共 1473 行)

  59| import type { PermissionResult } from '../../utils/permissions/PermissionResult.js'
  60| import { maybeRecordPluginHint } from '../../utils/plugins/hintRecommendation.js'
  61| import { exec } from '../../utils/Shell.js'
  62| import type { ExecResult } from '../../utils/ShellCommand.js'
  63| import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
  64| import { semanticBoolean } from '../../utils/semanticBoolean.js'
  65| import { semanticNumber } from '../../utils/semanticNumber.js'
  66| import { EndTruncatingAccumulator } from '../../utils/stringUtils.js'
  67| import { getTaskOutputPath } from '../../utils/task/diskOutput.js'
  68| import { TaskOutput } from '../../utils/task/TaskOutput.js'
  69| import { isOutputLineTruncated } from '../../utils/terminal.js'
  70| import {
  71|   buildLargeToolResultMessage,
  72|   ensureToolResultsDir,
  73|   generatePreview,
  74|   getToolResultPath,
  75|   PREVIEW_SIZE_BYTES,
  76| } from '../../utils/toolResultStorage.js'
  77| import { userFacingName as fileEditUserFacingName } from '../FileEditTool/UI.js'
  78| import { trackGitOperations } from '../shared/gitOperationTracking.js'

源码引用: src/tools/BashTool/BashTool.tsx · 第 95–172 行(共 1473 行)

  95| import {
  96|   BackgroundHint,
  97|   renderToolResultMessage,
  98|   renderToolUseErrorMessage,
  99|   renderToolUseMessage,
 100|   renderToolUseProgressMessage,
 101|   renderToolUseQueuedMessage,
 102| } from './UI.js'
 103| import {
 104|   buildImageToolResult,
 105|   isImageOutput,
 106|   resetCwdIfOutsideProject,
 107|   resizeShellImageOutput,
 108|   stdErrAppendShellResetMessage,
 109|   stripEmptyLines,
 110| } from './utils.js'
 111| 
 112| const EOL = '\n'
 113| 
 114| // Progress display constants
 115| const PROGRESS_THRESHOLD_MS = 2000 // Show progress after 2 seconds
 116| // In assistant mode, blocking bash auto-backgrounds after this many ms in the main agent
 117| const ASSISTANT_BLOCKING_BUDGET_MS = 15_000
 118| 
 119| // Search commands for collapsible display (grep, find, etc.)
 120| const BASH_SEARCH_COMMANDS = new Set([
 121|   'find',
 122|   'grep',
 123|   'rg',
 124|   'ag',
 125|   'ack',
 126|   'locate',
 127|   'which',
 128|   'whereis',
 129| ])
 130| 
 131| // Read/view commands for collapsible display (cat, head, etc.)
 132| const BASH_READ_COMMANDS = new Set([
 133|   'cat',
 134|   'head',
 135|   'tail',
 136|   'less',
 137|   'more',
 138|   // Analysis commands
 139|   'wc',
 140|   'stat',
 141|   'file',
 142|   'strings',
 143|   // Data processing — commonly used to parse/transform file content in pipes
 144|   'jq',
 145|   'awk',
 146|   'cut',
 147|   'sort',
 148|   'uniq',
 149|   'tr',
 150| ])
 151| 
 152| // Directory-listing commands for collapsible display (ls, tree, du).
 153| // Split from BASH_READ_COMMANDS so the summary says "Listed N directories"
 154| // instead of the misleading "Read N files".
 155| const BASH_LIST_COMMANDS = new Set(['ls', 'tree', 'du'])
 156| 
 157| // Commands that are semantic-neutral in any position — pure output/status commands
 158| // that don't change the read/search nature of the overall pipeline.
 159| // e.g. `ls dir && echo "---" && ls dir2` is still a read-only compound command.
 160| const BASH_SEMANTIC_NEUTRAL_COMMANDS = new Set([
 161|   'echo',
 162|   'printf',
 163|   'true',
 164|   'false',
 165|   ':', // bash no-op
 166| ])
 167| 
 168| // Commands that typically produce no stdout on success
 169| const BASH_SILENT_COMMANDS = new Set([
 170|   'mv',
 171|   'cp',
 172|   'rm',

call():执行主路径

call() 的第一分支是 _simulatedSedEdit:权限预览里 sed -i 已被解析为文件补丁,执行时不再起 shell,而是 applySedEdit 直接写盘,保证「用户看到的 diff = 实际写入」。常规路径走 runShellCommand 异步生成器,stdout/stderr 经 EndTruncatingAccumulator 截断,并通过 onProgress 推给 Executor。

几个特殊路径要一起读:

  • preventCwdChanges:子 agent(agentId 存在)禁止 cd 污染父会话 cwd
  • shouldUseSandbox:满足沙箱条件时走 SandboxManager
  • run_in_background:注册 LocalShellTask,result 里带 backgroundTaskId
  • assistant auto-background:阻塞命令超过预算后转后台
  • abortController:来自 StreamingToolExecutor 的 per-tool child controller,Bash 子进程监听 abort signal

Bash 一旦返回 is_error,StreamingToolExecutor 会取消并行 Bash sibling;Read/WebFetch 这类只读工具失败不会触发同样级联。

源码引用: src/tools/BashTool/BashTool.tsx · 第 846–950 行(共 1473 行)

 846|   async call(
 847|     input: BashToolInput,
 848|     toolUseContext,
 849|     _canUseTool?: CanUseToolFn,
 850|     parentMessage?: AssistantMessage,
 851|     onProgress?: ToolCallProgress<BashProgress>,
 852|   ) {
 853|     // Handle simulated sed edit - apply directly instead of running sed
 854|     // This ensures what the user previewed is exactly what gets written
 855|     if (input._simulatedSedEdit) {
 856|       return applySedEdit(
 857|         input._simulatedSedEdit,
 858|         toolUseContext,
 859|         parentMessage,
 860|       )
 861|     }
 862| 
 863|     const { abortController, getAppState, setAppState, setToolJSX } =
 864|       toolUseContext
 865| 
 866|     const stdoutAccumulator = new EndTruncatingAccumulator()
 867|     let stderrForShellReset = ''
 868|     let interpretationResult:
 869|       | ReturnType<typeof interpretCommandResult>
 870|       | undefined
 871| 
 872|     let progressCounter = 0
 873|     let wasInterrupted = false
 874|     let result: ExecResult
 875| 
 876|     const isMainThread = !toolUseContext.agentId
 877|     const preventCwdChanges = !isMainThread
 878| 
 879|     try {
 880|       // Use the new async generator version of runShellCommand
 881|       const commandGenerator = runShellCommand({
 882|         input,
 883|         abortController,
 884|         // Use the always-shared task channel so async agents' background
 885|         // bash tasks are actually registered (and killable on agent exit).
 886|         setAppState: toolUseContext.setAppStateForTasks ?? setAppState,
 887|         setToolJSX,
 888|         preventCwdChanges,
 889|         isMainThread,
 890|         toolUseId: toolUseContext.toolUseId,
 891|         agentId: toolUseContext.agentId,
 892|       })
 893| 
 894|       // Consume the generator and capture the return value
 895|       let generatorResult
 896|       do {
 897|         generatorResult = await commandGenerator.next()
 898|         if (!generatorResult.done && onProgress) {
 899|           const progress = generatorResult.value
 900|           onProgress({
 901|             toolUseID: `bash-progress-${progressCounter++}`,
 902|             data: {
 903|               type: 'bash_progress',
 904|               output: progress.output,
 905|               fullOutput: progress.fullOutput,
 906|               elapsedTimeSeconds: progress.elapsedTimeSeconds,
 907|               totalLines: progress.totalLines,
 908|               totalBytes: progress.totalBytes,
 909|               taskId: progress.taskId,
 910|               timeoutMs: progress.timeoutMs,
 911|             },
 912|           })
 913|         }
 914|       } while (!generatorResult.done)
 915| 
 916|       // Get the final result from the generator's return value
 917|       result = generatorResult.value
 918| 
 919|       trackGitOperations(input.command, result.code, result.stdout)
 920| 
 921|       const isInterrupt =
 922|         result.interrupted && abortController.signal.reason === 'interrupt'
 923| 
 924|       // stderr is interleaved in stdout (merged fd) — result.stdout has both
 925|       stdoutAccumulator.append((result.stdout || '').trimEnd() + EOL)
 926| 
 927|       // Interpret the command result using semantic rules
 928|       interpretationResult = interpretCommandResult(
 929|         input.command,
 930|         result.code,
 931|         result.stdout || '',
 932|         '',
 933|       )
 934| 
 935|       // Check for git index.lock error (stderr is in stdout now)
 936|       if (
 937|         result.stdout &&
 938|         result.stdout.includes(".git/index.lock': File exists")
 939|       ) {
 940|         logEvent('tengu_git_index_lock_error', {})
 941|       }
 942| 
 943|       if (interpretationResult.isError && !isInterrupt) {
 944|         // Only add exit code if it's actually an error
 945|         if (result.code !== 0) {
 946|           stdoutAccumulator.append(`Exit code ${result.code}`)
 947|         }
 948|       }
 949| 
 950|       if (!preventCwdChanges) {

源码引用: src/services/tools/StreamingToolExecutor.ts · 第 354–364 行(共 531 行)

 354|         if (isErrorResult) {
 355|           thisToolErrored = true
 356|           // Only Bash errors cancel siblings. Bash commands often have implicit
 357|           // dependency chains (e.g. mkdir fails → subsequent commands pointless).
 358|           // Read/WebFetch/etc are independent — one failure shouldn't nuke the rest.
 359|           if (tool.block.name === BASH_TOOL_NAME) {
 360|             this.hasErrored = true
 361|             this.erroredToolDescription = this.getToolDescription(tool)
 362|             this.siblingAbortController.abort('sibling_error')
 363|           }
 364|         }

mapToolResultToToolResultBlockParam

结果序列化处理多种形态:

  • structuredContent — 直接作为 content array
  • isImage — buildImageToolResult 转 image block
  • persistedOutputPath — buildLargeToolResultMessage 包装(仅模型见)
  • interrupted — stderr 追加 abort XML
  • backgroundTaskId — backgroundInfo 文案区分 user/assistant/auto background

extractSearchText 合并 stdout+stderr 供 transcript 搜索索引;与 renderToolResultMessage 可见文本应对齐(renderFidelity 测试约束)。

UI 使用 BashToolResultMessage 的 OutputLine 组件;never 展示 persisted 包装层。

源码引用: src/tools/BashTool/BashTool.tsx · 第 768–845 行(共 1473 行)

 768|   mapToolResultToToolResultBlockParam(
 769|     {
 770|       interrupted,
 771|       stdout,
 772|       stderr,
 773|       isImage,
 774|       backgroundTaskId,
 775|       backgroundedByUser,
 776|       assistantAutoBackgrounded,
 777|       structuredContent,
 778|       persistedOutputPath,
 779|       persistedOutputSize,
 780|     },
 781|     toolUseID,
 782|   ): ToolResultBlockParam {
 783|     // Handle structured content
 784|     if (structuredContent && structuredContent.length > 0) {
 785|       return {
 786|         tool_use_id: toolUseID,
 787|         type: 'tool_result',
 788|         content: structuredContent,
 789|       }
 790|     }
 791| 
 792|     // For image data, format as image content block for Claude
 793|     if (isImage) {
 794|       const block = buildImageToolResult(stdout, toolUseID)
 795|       if (block) return block
 796|     }
 797| 
 798|     let processedStdout = stdout
 799|     if (stdout) {
 800|       // Replace any leading newlines or lines with only whitespace
 801|       processedStdout = stdout.replace(/^(\s*\n)+/, '')
 802|       // Still trim the end as before
 803|       processedStdout = processedStdout.trimEnd()
 804|     }
 805| 
 806|     // For large output that was persisted to disk, build <persisted-output>
 807|     // message for the model. The UI never sees this — it uses data.stdout.
 808|     if (persistedOutputPath) {
 809|       const preview = generatePreview(processedStdout, PREVIEW_SIZE_BYTES)
 810|       processedStdout = buildLargeToolResultMessage({
 811|         filepath: persistedOutputPath,
 812|         originalSize: persistedOutputSize ?? 0,
 813|         isJson: false,
 814|         preview: preview.preview,
 815|         hasMore: preview.hasMore,
 816|       })
 817|     }
 818| 
 819|     let errorMessage = stderr.trim()
 820|     if (interrupted) {
 821|       if (stderr) errorMessage += EOL
 822|       errorMessage += '<error>Command was aborted before completion</error>'
 823|     }
 824| 
 825|     let backgroundInfo = ''
 826|     if (backgroundTaskId) {
 827|       const outputPath = getTaskOutputPath(backgroundTaskId)
 828|       if (assistantAutoBackgrounded) {
 829|         backgroundInfo = `Command exceeded the assistant-mode blocking budget (${ASSISTANT_BLOCKING_BUDGET_MS / 1000}s) and was moved to the background with ID: ${backgroundTaskId}. It is still running — you will be notified when it completes. Output is being written to: ${outputPath}. In assistant mode, delegate long-running work to a subagent or use run_in_background to keep this conversation responsive.`
 830|       } else if (backgroundedByUser) {
 831|         backgroundInfo = `Command was manually backgrounded by user with ID: ${backgroundTaskId}. Output is being written to: ${outputPath}`
 832|       } else {
 833|         backgroundInfo = `Command running in background with ID: ${backgroundTaskId}. Output is being written to: ${outputPath}`
 834|       }
 835|     }
 836| 
 837|     return {
 838|       tool_use_id: toolUseID,
 839|       type: 'tool_result',
 840|       content: [processedStdout, errorMessage, backgroundInfo]
 841|         .filter(Boolean)
 842|         .join('\n'),
 843|       is_error: interrupted,
 844|     }
 845|   },

bashToolHasPermission 入口

bashPermissions.ts 的 bashToolHasPermission(约 1663 行)是 Bash 特有权限大脑,在 permissions.ts 规则匹配之后调用。

典型检查链(读文件继续):

  • checkPermissionMode — plan/auto 模式约束
  • parseForSecurity / AST — 命令语义
  • checkPathConstraints — 工作目录与额外目录
  • checkSedConstraints — sed -i 转 FileEdit 权限
  • checkCommandOperatorPermissions — && || 管道组合
  • shouldUseSandbox + SandboxManager — 沙箱 auto-allow
  • classifyBashCommand — auto 模式分类器
  • startSpeculativeClassifierCheck — 权限弹窗等待时并行分类

返回 PermissionResult:allow / deny / ask。deny message 常含可操作建议(添加 Bash(pattern) 规则)。

体量大原因: 需覆盖 Windows path、heredoc、wildcard 规则、PowerShell 共存等边界。

源码引用: src/tools/BashTool/bashPermissions.ts · 第 1663–1720 行(共 2622 行)

1663| export async function bashToolHasPermission(
1664|   input: z.infer<typeof BashTool.inputSchema>,
1665|   context: ToolUseContext,
1666|   getCommandSubcommandPrefixFn = getCommandSubcommandPrefix,
1667| ): Promise<PermissionResult> {
1668|   let appState = context.getAppState()
1669| 
1670|   // 0. AST-based security parse. This replaces both tryParseShellCommand
1671|   // (the shell-quote pre-check) and the bashCommandIsSafe misparsing gate.
1672|   // tree-sitter produces either a clean SimpleCommand[] (quotes resolved,
1673|   // no hidden substitutions) or 'too-complex' — which is exactly the signal
1674|   // we need to decide whether splitCommand's output can be trusted.
1675|   //
1676|   // When tree-sitter WASM is unavailable OR the injection check is disabled
1677|   // via env var, we fall back to the old path (legacy gate at ~1370 runs).
1678|   const injectionCheckDisabled = isEnvTruthy(
1679|     process.env.CLAUDE_CODE_DISABLE_COMMAND_INJECTION_CHECK,
1680|   )
1681|   // GrowthBook killswitch for shadow mode — when off, skip the native parse
1682|   // entirely. Computed once; feature() must stay inline in the ternary below.
1683|   const shadowEnabled = feature('TREE_SITTER_BASH_SHADOW')
1684|     ? getFeatureValue_CACHED_MAY_BE_STALE('tengu_birch_trellis', true)
1685|     : false
1686|   // Parse once here; the resulting AST feeds both parseForSecurityFromAst
1687|   // and bashToolCheckCommandOperatorPermissions.
1688|   let astRoot = injectionCheckDisabled
1689|     ? null
1690|     : feature('TREE_SITTER_BASH_SHADOW') && !shadowEnabled
1691|       ? null
1692|       : await parseCommandRaw(input.command)
1693|   let astResult: ParseForSecurityResult = astRoot
1694|     ? parseForSecurityFromAst(input.command, astRoot)
1695|     : { kind: 'parse-unavailable' }
1696|   let astSubcommands: string[] | null = null
1697|   let astRedirects: Redirect[] | undefined
1698|   let astCommands: SimpleCommand[] | undefined
1699|   let shadowLegacySubs: string[] | undefined
1700| 
1701|   // Shadow-test tree-sitter: record its verdict, then force parse-unavailable
1702|   // so the legacy path stays authoritative. parseCommand stays gated on
1703|   // TREE_SITTER_BASH (not SHADOW) so legacy internals remain pure regex.
1704|   // One event per bash call captures both divergence AND unavailability
1705|   // reasons; module-load failures are separately covered by the
1706|   // session-scoped tengu_tree_sitter_load event.
1707|   if (feature('TREE_SITTER_BASH_SHADOW')) {
1708|     const available = astResult.kind !== 'parse-unavailable'
1709|     let tooComplex = false
1710|     let semanticFail = false
1711|     let subsDiffer = false
1712|     if (available) {
1713|       tooComplex = astResult.kind === 'too-complex'
1714|       semanticFail =
1715|         astResult.kind === 'simple' && !checkSemantics(astResult.commands).ok
1716|       const tsSubs =
1717|         astResult.kind === 'simple'
1718|           ? astResult.commands.map(c => c.text)
1719|           : undefined
1720|       const legacySubs = splitCommand(input.command)

源码引用: src/tools/BashTool/bashPermissions.ts · 第 1–80 行(共 2622 行)

   1| import { feature } from 'bun:bundle'
   2| import { APIUserAbortError } from '@anthropic-ai/sdk'
   3| import type { z } from 'zod/v4'
   4| import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
   5| import {
   6|   type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
   7|   logEvent,
   8| } from '../../services/analytics/index.js'
   9| import type { ToolPermissionContext, ToolUseContext } from '../../Tool.js'
  10| import type { PendingClassifierCheck } from '../../types/permissions.js'
  11| import { count } from '../../utils/array.js'
  12| import {
  13|   checkSemantics,
  14|   nodeTypeId,
  15|   type ParseForSecurityResult,
  16|   parseForSecurityFromAst,
  17|   type Redirect,
  18|   type SimpleCommand,
  19| } from '../../utils/bash/ast.js'
  20| import {
  21|   type CommandPrefixResult,
  22|   extractOutputRedirections,
  23|   getCommandSubcommandPrefix,
  24|   splitCommand_DEPRECATED,
  25| } from '../../utils/bash/commands.js'
  26| import { parseCommandRaw } from '../../utils/bash/parser.js'
  27| import { tryParseShellCommand } from '../../utils/bash/shellQuote.js'
  28| import { getCwd } from '../../utils/cwd.js'
  29| import { logForDebugging } from '../../utils/debug.js'
  30| import { isEnvTruthy } from '../../utils/envUtils.js'
  31| import { AbortError } from '../../utils/errors.js'
  32| import type {
  33|   ClassifierBehavior,
  34|   ClassifierResult,
  35| } from '../../utils/permissions/bashClassifier.js'
  36| import {
  37|   classifyBashCommand,
  38|   getBashPromptAllowDescriptions,
  39|   getBashPromptAskDescriptions,
  40|   getBashPromptDenyDescriptions,
  41|   isClassifierPermissionsEnabled,
  42| } from '../../utils/permissions/bashClassifier.js'
  43| import type {
  44|   PermissionDecisionReason,
  45|   PermissionResult,
  46| } from '../../utils/permissions/PermissionResult.js'
  47| import type {
  48|   PermissionRule,
  49|   PermissionRuleValue,
  50| } from '../../utils/permissions/PermissionRule.js'
  51| import { extractRules } from '../../utils/permissions/PermissionUpdate.js'
  52| import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js'
  53| import { permissionRuleValueToString } from '../../utils/permissions/permissionRuleParser.js'
  54| import {
  55|   createPermissionRequestMessage,
  56|   getRuleByContentsForTool,
  57| } from '../../utils/permissions/permissions.js'
  58| import {
  59|   parsePermissionRule,
  60|   type ShellPermissionRule,
  61|   matchWildcardPattern as sharedMatchWildcardPattern,
  62|   permissionRuleExtractPrefix as sharedPermissionRuleExtractPrefix,
  63|   suggestionForExactCommand as sharedSuggestionForExactCommand,
  64|   suggestionForPrefix as sharedSuggestionForPrefix,
  65| } from '../../utils/permissions/shellRuleMatching.js'
  66| import { getPlatform } from '../../utils/platform.js'
  67| import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
  68| import { jsonStringify } from '../../utils/slowOperations.js'
  69| import { windowsPathToPosixPath } from '../../utils/windowsPaths.js'
  70| import { BashTool } from './BashTool.js'
  71| import { checkCommandOperatorPermissions } from './bashCommandHelpers.js'
  72| import {
  73|   bashCommandIsSafeAsync_DEPRECATED,
  74|   stripSafeHeredocSubstitutions,
  75| } from './bashSecurity.js'
  76| import { checkPermissionMode } from './modeValidation.js'
  77| import { checkPathConstraints } from './pathValidation.js'
  78| import { checkSedConstraints } from './sedValidation.js'
  79| import { shouldUseSandbox } from './shouldUseSandbox.js'
  80| 

只读验证与沙箱

readOnlyValidation.ts 的 checkReadOnlyConstraints 分析 compound 命令:禁止 redirect 写文件、禁止 destructive 命令、cd 出项目根等。

shouldUseSandbox.ts 结合 env(CLAUDE_CODE_BASH_SANDBOX)与命令特征决定是否沙箱执行。沙箱成功时 permission 可 auto-allow,减少弹窗。

modeValidation.ts 处理 plan 模式下 Bash 限制(只读探索 vs 需 exit plan)。

工程练习: 对比 isReadOnly 与 bashToolHasPermission 的 allow 条件——前者决定并发,后者决定能否执行。

源码引用: src/tools/BashTool/readOnlyValidation.ts · 第 1–60 行(共 1991 行)

   1| import type { z } from 'zod/v4'
   2| import { getOriginalCwd } from '../../bootstrap/state.js'
   3| import {
   4|   extractOutputRedirections,
   5|   splitCommand_DEPRECATED,
   6| } from '../../utils/bash/commands.js'
   7| import { tryParseShellCommand } from '../../utils/bash/shellQuote.js'
   8| import { getCwd } from '../../utils/cwd.js'
   9| import { isCurrentDirectoryBareGitRepo } from '../../utils/git.js'
  10| import type { PermissionResult } from '../../utils/permissions/PermissionResult.js'
  11| import { getPlatform } from '../../utils/platform.js'
  12| import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
  13| import {
  14|   containsVulnerableUncPath,
  15|   DOCKER_READ_ONLY_COMMANDS,
  16|   EXTERNAL_READONLY_COMMANDS,
  17|   type FlagArgType,
  18|   GH_READ_ONLY_COMMANDS,
  19|   GIT_READ_ONLY_COMMANDS,
  20|   PYRIGHT_READ_ONLY_COMMANDS,
  21|   RIPGREP_READ_ONLY_COMMANDS,
  22|   validateFlags,
  23| } from '../../utils/shell/readOnlyCommandValidation.js'
  24| import type { BashTool } from './BashTool.js'
  25| import { isNormalizedGitCommand } from './bashPermissions.js'
  26| import { bashCommandIsSafe_DEPRECATED } from './bashSecurity.js'
  27| import {
  28|   COMMAND_OPERATION_TYPE,
  29|   PATH_EXTRACTORS,
  30|   type PathCommand,
  31| } from './pathValidation.js'
  32| import { sedCommandIsAllowedByAllowlist } from './sedValidation.js'
  33| 
  34| // Unified command validation configuration system
  35| type CommandConfig = {
  36|   // A Record mapping from the command (e.g. `xargs` or `git diff`) to its safe flags and the values they accept
  37|   safeFlags: Record<string, FlagArgType>
  38|   // An optional regex that is used for additional validation beyond flag parsing
  39|   regex?: RegExp
  40|   // An optional callback for additional custom validation logic. Returns true if the command is dangerous,
  41|   // false if it appears to be safe. Meant to be used in conjunction with the safeFlags-based validation.
  42|   additionalCommandIsDangerousCallback?: (
  43|     rawCommand: string,
  44|     args: string[],
  45|   ) => boolean
  46|   // When false, the tool does NOT respect POSIX `--` end-of-options.
  47|   // validateFlags will continue checking flags after `--` instead of breaking.
  48|   // Default: true (most tools respect `--`).
  49|   respectsDoubleDash?: boolean
  50| }
  51| 
  52| // Shared safe flags for fd and fdfind (Debian/Ubuntu package name)
  53| // SECURITY: -x/--exec and -X/--exec-batch are deliberately excluded —
  54| // they execute arbitrary commands for each search result.
  55| const FD_SAFE_FLAGS: Record<string, FlagArgType> = {
  56|   '-h': 'none',
  57|   '--help': 'none',
  58|   '-V': 'none',
  59|   '--version': 'none',
  60|   '-H': 'none',

源码引用: src/tools/BashTool/shouldUseSandbox.ts · 第 1–50 行(共 154 行)

   1| import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
   2| import { splitCommand_DEPRECATED } from '../../utils/bash/commands.js'
   3| import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
   4| import { getSettings_DEPRECATED } from '../../utils/settings/settings.js'
   5| import {
   6|   BINARY_HIJACK_VARS,
   7|   bashPermissionRule,
   8|   matchWildcardPattern,
   9|   stripAllLeadingEnvVars,
  10|   stripSafeWrappers,
  11| } from './bashPermissions.js'
  12| 
  13| type SandboxInput = {
  14|   command?: string
  15|   dangerouslyDisableSandbox?: boolean
  16| }
  17| 
  18| // NOTE: excludedCommands is a user-facing convenience feature, not a security boundary.
  19| // It is not a security bug to be able to bypass excludedCommands — the sandbox permission
  20| // system (which prompts users) is the actual security control.
  21| function containsExcludedCommand(command: string): boolean {
  22|   // Check dynamic config for disabled commands and substrings (only for ants)
  23|   if (process.env.USER_TYPE === 'ant') {
  24|     const disabledCommands = getFeatureValue_CACHED_MAY_BE_STALE<{
  25|       commands: string[]
  26|       substrings: string[]
  27|     }>('tengu_sandbox_disabled_commands', { commands: [], substrings: [] })
  28| 
  29|     // Check if command contains any disabled substrings
  30|     for (const substring of disabledCommands.substrings) {
  31|       if (command.includes(substring)) {
  32|         return true
  33|       }
  34|     }
  35| 
  36|     // Check if command starts with any disabled commands
  37|     try {
  38|       const commandParts = splitCommand_DEPRECATED(command)
  39|       for (const part of commandParts) {
  40|         const baseCommand = part.trim().split(' ')[0]
  41|         if (baseCommand && disabledCommands.commands.includes(baseCommand)) {
  42|           return true
  43|         }
  44|       }
  45|     } catch {
  46|       // If we can't parse the command (e.g., malformed bash syntax),
  47|       // treat it as not excluded to allow other validation checks to handle it
  48|       // This prevents crashes when rendering tool use messages
  49|     }
  50|   }

与 StreamingToolExecutor 的 Bash 级联

StreamingToolExecutor 对 Bash 有特殊错误传播:当某 Bash tool_result is_error=true,设置 hasErrored 并 abort siblingAbortController('sibling_error')。并行 Bash 收到 synthetic cancel message。

Read/WebFetch 等独立工具错误不级联——注释说明 mkdir 失败不应取消并行 Read。

Bash call 内 abortController 是 siblingAbortController 的子 controller;权限拒绝 abort 需 bubble 到 query controller(#21056 ExitPlanMode 回归)。

理解 Bash 调试时,同时打开 StreamingToolExecutor.executeTool 与 BashTool.call。

源码引用: src/services/tools/StreamingToolExecutor.ts · 第 354–364 行(共 531 行)

 354|         if (isErrorResult) {
 355|           thisToolErrored = true
 356|           // Only Bash errors cancel siblings. Bash commands often have implicit
 357|           // dependency chains (e.g. mkdir fails → subsequent commands pointless).
 358|           // Read/WebFetch/etc are independent — one failure shouldn't nuke the rest.
 359|           if (tool.block.name === BASH_TOOL_NAME) {
 360|             this.hasErrored = true
 361|             this.erroredToolDescription = this.getToolDescription(tool)
 362|             this.siblingAbortController.abort('sibling_error')
 363|           }
 364|         }

源码引用: src/services/tools/StreamingToolExecutor.ts · 第 294–318 行(共 531 行)

 294|       // Per-tool child controller. Lets siblingAbortController kill running
 295|       // subprocesses (Bash spawns listen to this signal) when a Bash error
 296|       // cascades. Permission-dialog rejection also aborts this controller
 297|       // (PermissionContext.ts cancelAndAbort) — that abort must bubble up to
 298|       // the query controller so the query loop's post-tool abort check ends
 299|       // the turn. Without bubble-up, ExitPlanMode "clear context + auto"
 300|       // sends REJECT_MESSAGE to the model instead of aborting (#21056 regression).
 301|       const toolAbortController = createChildAbortController(
 302|         this.siblingAbortController,
 303|       )
 304|       toolAbortController.signal.addEventListener(
 305|         'abort',
 306|         () => {
 307|           if (
 308|             toolAbortController.signal.reason !== 'sibling_error' &&
 309|             !this.toolUseContext.abortController.signal.aborted &&
 310|             !this.discarded
 311|           ) {
 312|             this.toolUseContext.abortController.abort(
 313|               toolAbortController.signal.reason,
 314|             )
 315|           }
 316|         },
 317|         { once: true },
 318|       )

源码目录

点击 bashPermissions.ts、UI.tsx 等关联文件。Bash 权限 AST 依赖 utils/bash/ast.ts,跨模块阅读时一并打开。

动手练习

  1. 构造纯 grep 管道与 grep|tee 写文件管道,对比 isReadOnly 结果
  2. 在 REPL 并行触发两个只读 Bash,观察 StreamingToolExecutor 是否同时 executing
  3. 故意让第一个 Bash 失败,确认 sibling 收到 Cancelled: parallel tool call 消息
  4. 阅读 sedEditParser,找出 sed -i 如何转为 _simulatedSedEdit
  5. 对照 toAutoClassifierInput 返回的 command 与 buildYoloRejectionMessage 在 deny 时的模型续写行为

本章小结与延伸

BashTool = 执行 + 安全 + 展示三合一。权限细节延伸 utils/permissions/bashClassifier;执行器延伸 streaming-executor。 继续学习:

  • streaming-executor
  • permissions
Prev
tool-interface · Tool 契约与注册表
Next
streaming-executor · 流式工具并发调度