7. BashTool - Shell 命令执行

所属分组:工具系统

概述

BashTool 是 Claude Code 中最具"破坏力"也最常用的工具之一:它把模型的文本输出直接交给系统 shell 执行,是模型与操作系统交互的"万能钥匙"。正因为能力强大,BashTool 的实现同时承担了三重职责——给模型清晰的 prompt 指引、对命令输出做格式化与安全处理、在终端里渲染出可读的执行过程与结果。

本工具目录下文件众多:BashTool.tsx 承载核心 call 与权限逻辑;prompt.ts 生成模型可见的工具描述与 Git/PR 操作长指引;utils.ts 处理命令输出的截断、图片识别、cwd 重置等;UI.tsx 负责所有 React 渲染入口。配套还有 bashPermissions.tsbashSecurity.tsshouldUseSandbox.tscommandSemantics.ts 等安全模块,以及底层的 utils/Shell.tsutils/ShellCommand.ts 提供进程执行能力。

本文聚焦 prompt.ts、utils.ts、UI.tsx 三个核心文件,剖析 BashTool 如何在"模型友好 / 安全可控 / UI 清晰"三者之间取得平衡。

源码位置

  • [tools/BashTool/prompt.ts](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/BashTool/prompt.ts)
  • [tools/BashTool/utils.ts](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/BashTool/utils.ts)
  • [tools/BashTool/UI.tsx](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/BashTool/UI.tsx)
  • [tools/BashTool/BashTool.tsx](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/BashTool/BashTool.tsx)
  • [utils/Shell.ts](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/utils/Shell.ts)

核心实现分析

1. prompt.ts:分层组装的工具描述

getSimplePrompt() 是 BashTool 注入到 system prompt 的主体文本。它的构造方式很特别——不是单块字符串,而是由多个独立函数拼接而成,每一层负责一类关注点:

  • 工具偏好层:明确告诉模型哪些操作应该用专用工具而非 Bash(File search: Use GlobRead files: Use FileReadTool 等)。这层策略直接降低模型误用 Bash 做文件操作的几率。
  • 指令层:处理多条命令的并行/串行规则、git 命令的安全约束、sleep 命令的反模式警告。
  • 沙箱层getSimpleSandboxSection() 注入文件系统/网络沙箱的运行时配置。
  • Git/PR 层getCommitAndPRInstructions() 是一段非常长的内嵌指引,覆盖 commit 流程、PR 创建、git 安全协议。

值得注意的是工具偏好与 avoidCommands 的动态生成:

const avoidCommands = embedded
  ? '`cat`, `head`, `tail`, `sed`, `awk`, or `echo`'
  : '`find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or `echo`'

当 ant 原生构建把 find/grep 别名到内嵌的 bfs/ugrep 时,prompt 主动让模型使用这些命令;否则劝阻。这是 prompt 与运行时环境双向适配的典型例子。

2. 沙箱配置的 token 优化

getSimpleSandboxSection() 在注入沙箱配置前会做一道去重处理:

// SandboxManager merges config from multiple sources (settings layers, defaults,
// CLI flags) without deduping, so paths like ~/.cache appear 3× in allowOnly.
// Dedup here before inlining into the prompt — affects only what the model sees,
// not sandbox enforcement. Saves ~150-200 tokens/request when sandbox is enabled.
function dedup<T>(arr: T[] | undefined): T[] | undefined {
  if (!arr || arr.length === 0) return arr
  return [...new Set(arr)]
}

注释揭示了细节:SandboxManager 把多层 settings 合并但不去重,导致 ~/.cache 这样的路径在 allowOnly 中出现三次。这里去重只影响模型看到的文本(不影响实际沙箱执行),每次请求节省约 150-200 tokens。还有一处把 per-UID 临时目录(如 /private/tmp/claude-1001/)归一化为 $TMPDIR,“避免破坏跨用户的全局 prompt cache”。

3. dangerouslyDisableSandbox 的两套策略

getSimpleSandboxSection() 根据 allowUnsandboxedCommands 输出两套截然不同的指引。当允许越狱时:

'You should always default to running commands within the sandbox. Do NOT attempt to set `dangerouslyDisableSandbox: true` unless:',
// ...
"When you see evidence of sandbox-caused failure:",
[
  "Immediately retry with `dangerouslyDisableSandbox: true` (don't ask, just do it)",
  'Briefly explain what sandbox restriction likely caused the failure...',
  'This will prompt the user for permission',
],

策略是"看到沙箱失败证据就立即重试并解释"——既不让模型反复纠缠用户,又保留了 audit trail。而不允许越狱时则是硬性约束:“Commands cannot run outside the sandbox under any circumstances.”

4. Git 安全协议的 fail-safe 设计

getCommitAndPRInstructions() 中嵌入了大量"NEVER"约束:

- NEVER update the git config
- NEVER run destructive git commands (push --force, reset --hard, checkout ., restore ., clean -f, branch -D) unless...
- NEVER skip hooks (--no-verify, --no-gpg-sign, etc) unless...
- NEVER run force push to main/master, warn the user if they request it
- CRITICAL: Always create NEW commits rather than amending, unless the user explicitly requests a git amend...

特别值得品味的是"amend"那条的注释:当 pre-commit hook 失败时,commit 实际并未发生,此时若 --amend 会修改前一次提交,可能毁掉前一次的工作。模型很容易踩这个坑,所以用 CRITICAL 标记并给出替代流程(fix issue → re-stage → create NEW commit)。

5. utils.ts:命令输出的多形态处理

formatOutput() 是 utils.ts 中最关键的函数之一,它要处理两类截然不同的输出——纯文本与图片 data URI:

export function formatOutput(content: string): {
  totalLines: number
  truncatedContent: string
  isImage?: boolean
} {
  const isImage = isImageOutput(content)
  if (isImage) {
    return { totalLines: 1, truncatedContent: content, isImage }
  }

  const maxOutputLength = getMaxOutputLength()
  if (content.length <= maxOutputLength) {
    return { totalLines: countCharInString(content, '\n') + 1, truncatedContent: content, isImage }
  }

  const truncatedPart = content.slice(0, maxOutputLength)
  const remainingLines = countCharInString(content, '\n', maxOutputLength) + 1
  const truncated = `${truncatedPart}\n\n... [${remainingLines} lines truncated] ...`
  // ...
}

isImageOutput() 用正则匹配 data:image/...;base64, 前缀;命中后内容直接透传给 buildImageToolResult() 转成 Anthropic SDK 的 image block。这种"shell 命令直接吐出图片"的能力让模型可以运行 matplotlib 脚本后立即看到结果。

6. 图片输出的尺寸压缩

resizeShellImageOutput() 解决了一个微妙的问题——shell stdout 被截断到 getMaxOutputLength() 时,截断的 base64 会解码成损坏的图片。函数会从 spill 文件重新读取完整内容,再做尺寸压缩:

// Caps dimensions too: compressImageBuffer only checks byte size, so
// a small-but-high-DPI PNG (e.g. matplotlib at dpi=300) sails through at full
// resolution and poisons many-image requests (CC-304).
const resized = await maybeResizeAndDownsampleImageBuffer(buf, buf.length, ext)

注释里的 CC-304 是真实案例——高 DPI PNG 虽然字节数小但分辨率高,会在多图请求中"中毒"。这里强制下采样避免该问题。MAX_IMAGE_FILE_SIZE 设为 20MB,超过直接返回 null(让调用方回退到文本处理)。

7. cwd 重置机制

resetCwdIfOutsideProject() 是 BashTool 在每次调用后保护工作目录一致性的关键:

export function resetCwdIfOutsideProject(
  toolPermissionContext: ToolPermissionContext,
): boolean {
  const cwd = getCwd()
  const originalCwd = getOriginalCwd()
  const shouldMaintain = shouldMaintainProjectWorkingDir()
  if (
    shouldMaintain ||
    (cwd !== originalCwd &&
      !pathInAllowedWorkingPath(cwd, toolPermissionContext))
  ) {
    setCwd(originalCwd)
    if (!shouldMaintain) {
      logEvent('tengu_bash_tool_reset_to_original_dir', {})
      return true
    }
  }
  return false
}

注意 fast path:当 cwd 没变(cwd === originalCwd)时直接跳过 pathInAllowedWorkingPath 的系统调用——因为 originalCwd 一定在允许的工作目录里。这是高频路径的优化。当 cwd 漂出允许目录时,自动重置并打点上报。

8. UI.tsx:渲染入口与 sed 编辑特例

renderToolUseMessage() 处理工具调用气泡。它对 sed -i 这类原地编辑做了特例:

export function renderToolUseMessage(input, { verbose, theme: _theme }) {
  const { command } = input
  if (!command) return null

  // Render sed in-place edits like file edits (show file path only)
  const sedInfo = parseSedEditCommand(command)
  if (sedInfo) {
    return verbose ? sedInfo.filePath : getDisplayPath(sedInfo.filePath)
  }
  // ...
}

parseSedEditCommand()sed -i 's/foo/bar/' file.txt 识别成文件编辑操作,UI 上就只显示文件路径——视觉上等同于 FileEditTool,让用户一眼看出"这是一次文件修改"而不是"一条 shell 命令"。这是 UI 把语义还原为意图的范例。

9. 命令显示的截断策略

非 verbose 模式下命令显示有两道截断:

const MAX_COMMAND_DISPLAY_LINES = 2
const MAX_COMMAND_DISPLAY_CHARS = 160

if (!verbose) {
  const lines = command.split('\n')
  if (isFullscreenEnvEnabled()) {
    const label = extractBashCommentLabel(command)
    if (label) { /* 用注释作为 label */ }
  }
  const needsLineTruncation = lines.length > MAX_COMMAND_DISPLAY_LINES
  const needsCharTruncation = command.length > MAX_COMMAND_DISPLAY_CHARS
  if (needsLineTruncation || needsCharTruncation) {
    // 先按行截,再按字符截
  }
}

extractBashCommentLabel() 在全屏模式下会提取命令开头的注释(如 # run tests\npytest)作为 label 显示——这是给"用注释标注意图"的命令风格的奖励。

10. BackgroundHint 与 ctrl+b 后台化

BackgroundHint 组件让用户能把前台运行中的命令一键后台化:

export function BackgroundHint({ onBackground }) {
  // ...
  useKeybinding("task:background", handleBackground, { context: "Task" })
  const baseShortcut = useShortcutDisplay("task:background", "Task", "ctrl+b")
  const shortcut = env.terminal === "tmux" && baseShortcut === "ctrl+b"
    ? "ctrl+b ctrl+b (twice)"
    : baseShortcut
  // ...
}

注意 tmux 环境下的特殊处理——tmux 默认前缀键也是 ctrl+b,所以提示用户要按两次。这种"环境感知的快捷键提示"是细节控的体现。backgroundAll() 真正执行后台化,把所有前台命令转成 LocalShellTask。

11. 进度与结果的渲染分工

renderToolUseProgressMessageShellProgressMessage 组件展示实时输出(带 elapsedTime、totalBytes、timeoutMs 等);renderToolResultMessageBashToolResultMessage 展示最终结果,超时信息从最后一条 progress 中提取:

export function renderToolResultMessage(content, progressMessagesForMessage, { verbose, theme, tools, style }) {
  const lastProgress = progressMessagesForMessage.at(-1)
  const timeoutMs = lastProgress?.data?.timeoutMs
  return <BashToolResultMessage content={content} verbose={verbose} timeoutMs={timeoutMs} />
}

renderToolUseQueuedMessage 简短地显示"Waiting…"——当工具排队等锁时给用户即时反馈。

12. 与 Shell.ts/ShellCommand.ts 的关系

utils.ts 直接调用了 setCwd from utils/Shell.ts。BashTool 的核心 call 实现在 BashTool.tsx 中通过 ShellCommand 抽象执行命令——ShellCommand 处理进程派生、stdout/stderr 流捕获、超时、kill 信号等底层细节,让 BashTool 可以专注业务逻辑(权限、沙箱决策、输出后处理)。这种"高层工具 + 低层 Shell 抽象"的分层让 PowerShellTool 可以复用同一套 Shell 基础设施。

关键设计要点

  1. prompt 分层生成getSimplePrompt 由工具偏好、指令、沙箱、Git/PR 四层函数拼接,每层独立可演化,沙箱配置还能 token 优化去重。
  2. 环境双向适配:embedded search tools、tmux 终端、ant 用户类型等环境差异都会反映到 prompt 文本和 UI 提示里,避免"一刀切"的失配。
  3. 输出多形态识别formatOutput + isImageOutput + resizeShellImageOutput 让 Bash 输出能优雅处理纯文本、图片 data URI、超大输出三类形态。
  4. sed 编辑的 UI 还原parseSedEditCommandsed -i 视为文件编辑操作,UI 上只显示文件路径,把"shell 命令"还原为"文件修改"的语义。
  5. Git 安全协议 fail-safe:amend、destructive ops、skip hooks 等危险操作都有"NEVER… unless"约束,并在 pre-commit hook 失败场景给出明确替代流程。

与其他模块的关系

  • Shell.ts / ShellCommand.ts:提供进程派生与流管理,BashTool 在其之上构建权限/沙箱/输出处理逻辑。
  • SandboxManagerutils/sandbox/sandbox-adapter.ts 提供 getFsReadConfiggetFsWriteConfiggetNetworkRestrictionConfig 等,BashTool 的 prompt 把这些配置内联给模型看,运行时由 sandbox 在进程层强制执行。
  • permissions 系统bashPermissions.tsbashSecurity.tscommandSemantics.tsdestructiveCommandWarning.ts 共同决定一条命令是否需要用户确认;shouldUseSandbox.ts 决定是否进入沙箱模式。
  • tasks/LocalShellTaskbackgroundAll 把前台命令转后台任务,由任务系统统一调度与通知。
  • FileEditTool/FileReadTool:BashTool 的 prompt 主动引导模型使用这些专用工具,sed 解析又把 sed 编辑在 UI 上对齐到 FileEdit 体验,形成闭环。

小结

BashTool 是"用 prompt 写运行时规则、用 utils 做输出兜底、用 UI 还原操作语义"的集大成者。它的 prompt.ts 不是简单文本,而是分层组装、token 优化、环境感知的运行时策略;utils.ts 同时处理文本截断、图片解码、cwd 保护三件不同的事;UI.tsx 把 sed -i 渲染成文件编辑、把 ctrl+b 后台化做成 tmux 兼容。理解 BashTool 的设计哲学,就能理解 Claude Code 工具系统在"模型可控性、运行时安全、用户体验"三角上的取舍方式。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐