Claude Code Wiki
首页 深入解析 工具系统

BashTool:命令执行与安全沙箱

高级 工具系统

BashTool 是 Claude Code 中最核心的工具之一,负责在受控环境中执行 Shell 命令。它实现了多层安全防护机制,包括 AST 级别的命令解析、细粒度权限控制、可插拔的沙箱系统,以及智能的只读命令识别。本页将深入剖析其架构设计、安全验证流程、沙箱机制以及执行流水线。

架构概览:多层防御体系

BashTool 采用**深度防御(Defense in Depth)**策略,通过多个独立的安全层逐级过滤和验证命令。每一层都可以阻止危险操作,确保只有安全的命令才能到达执行层。

graph TB
    A[Tool Invocation] --> B[Input Validation]
    B --> C[Permission Check]
    C --> D{Security Validation}
    D -->|Safe| E[Sandbox Decision]
    D -->|Dangerous| F[Ask/Deny]
    E -->|Sandbox Enabled| G[Sandbox Execution]
    E -->|Sandbox Disabled| H[Direct Execution]
    G --> I[Output Processing]
    H --> I
    I --> J[Result Interpretation]
    J --> K[Return to Model]
    
    subgraph Security Layers
        D
        D1[Tree-sitter AST Parsing]
        D2[Dangerous Pattern Detection]
        D3[Injection Prevention]
        D4[Path Constraint Validation]
    end
    
    D --> D1
    D1 --> D2
    D2 --> D3
    D3 --> D4

核心组件职责划分

组件文件路径职责
BashTool.tsxsrc/tools/BashTool/BashTool.tsx工具入口、执行编排、结果映射
bashSecurity.tssrc/tools/BashTool/bashSecurity.tsAST 解析、危险模式检测、注入防御
bashPermissions.tssrc/tools/BashTool/bashPermissions.ts权限规则匹配、分类器集成
shouldUseSandbox.tssrc/tools/BashTool/shouldUseSandbox.ts沙箱启用决策逻辑
sandbox-adapter.tssrc/utils/sandbox/sandbox-adapter.ts沙箱运行时适配器
readOnlyValidation.tssrc/tools/BashTool/readOnlyValidation.ts只读命令白名单验证
commandSemantics.tssrc/tools/BashTool/commandSemantics.ts退出码语义解释

Sources: BashTool.tsx, bashSecurity.ts, bashPermissions.ts, shouldUseSandbox.ts

安全验证流水线:从 AST 到执行

Tree-sitter AST 解析层

BashTool 使用 Tree-sitter 对 Shell 命令进行完整的语法树解析,而非简单的正则匹配。这种方法能准确识别命令结构,避免误报和绕过攻击。

sequenceDiagram
    participant Tool as BashTool
    participant Parser as Tree-sitter Parser
    participant Validators as Security Validators
    participant Permission as Permission System
    
    Tool->>Parser: parseForSecurity(command)
    Parser->>Parser: Build AST Tree
    Parser->>Parser: Extract SimpleCommands
    Parser-->>Tool: ParseResult
    
    loop For each subcommand
        Tool->>Validators: validateIncompleteCommands()
        Validators-->>Tool: passthrough/ask
        Tool->>Validators: validateDangerousPatterns()
        Validators-->>Tool: passthrough/ask
        Tool->>Validators: validateInjectionVectors()
        Validators-->>Tool: passthrough/ask
    end
    
    Tool->>Permission: bashToolHasPermission()
    Permission-->>Tool: allow/ask/deny

解析结果类型

type ParseForSecurityResult = 
  | { kind: 'simple'; commands: SimpleCommand[]; redirects: Redirect[] }
  | { kind: 'unavailable' }  // Tree-sitter 不可用
  | { kind: 'too-complex' }  // 嵌套层级过深

AST 解析能够精确识别:

  • 命令替换$(cmd)`cmd`<(cmd)>(cmd)
  • 变量展开${var}$var$[arithmetic]
  • 重定向操作>file>>file<file2>&1
  • 复合命令cmd1 && cmd2cmd1 || cmd2cmd1 ; cmd2cmd1 | cmd2

Sources: bashSecurity.ts, ast.ts

危险模式检测器

bashSecurity.ts 实现了 20+ 个独立的验证器,每个专注于特定攻击向量:

命令替换攻击向量

  • $() 命令替换:可能执行任意代码
  • <() / >() 进程替换:创建临时文件描述符
  • ${} 参数展开:可能触发间接执行
  • Zsh 特有:=cmd 展开为 $(which cmd),可绕过二进制黑名单

Zsh 危险命令黑名单

const ZSH_DANGEROUS_COMMANDS = new Set([
  'zmodload',    // 模块加载器 - 访问危险模块的入口
  'emulate',     // 模拟模式 - 可执行任意代码
  'sysopen',     // 系统调用 - 文件描述符操作
  'sysread',     // 读取任意 fd
  'syswrite',    // 写入任意 fd
  'zpty',        // 伪终端 - 命令执行
  'ztcp',        // TCP 连接 - 网络外泄
  'zsocket',     // Unix socket - IPC 攻击
  // ... 更多 zsh/files 内置命令
])

引号注入检测: 系统会检测引号状态不同步攻击,例如:

echo 'unclosed
# 后续命令会被误解析

Sources: bashSecurity.ts, bashSecurity.ts

Heredoc 安全验证

Heredoc(Here Document)是 Shell 中的多行文本输入机制,但也可被滥用执行任意代码。BashTool 实现了严格的 heredoc 安全校验:

安全 Heredoc 模式

# ✅ 安全:单引号定界符,无变量展开
result=$(cat <<'EOF'
This is literal text
No variable expansion: $HOME
EOF
)

危险 Heredoc 模式

# ❌ 危险:未引用定界符,会展开变量和命令
cat <<EOF
Current dir: $(pwd)
User: $USER
EOF

验证器采用逐行匹配(而非正则),精确复现 Bash 的 heredoc 关闭行为:

  1. 定位 <<'DELIM' 模式
  2. 逐行查找第一个完全匹配的定界符
  3. 验证定界符后只能有空白和 )
  4. 确保 ) 出现在正确位置

Sources: bashSecurity.ts

沙箱系统:隔离执行环境

沙箱架构设计

BashTool 集成了 @anthropic-ai/sandbox-runtime 包,提供操作系统级别的隔离。沙箱使用 Bubblewrap(Linux)或 Seatbelt(macOS)限制进程权限。

graph LR
    A[Command String] --> B{shouldUseSandbox?}
    B -->|Yes| C[SandboxManager.wrapWithSandbox]
    B -->|No| D[Direct Execution]
    
    C --> E[Build Sandbox Config]
    E --> F[Filesystem Restrictions]
    E --> G[Network Restrictions]
    E --> H[Resource Limits]
    
    F --> I[Allow: Project Dir]
    F --> J[Allow: Temp Dir]
    F --> K[Deny: ~/.ssh, ~/.gnupg]
    
    G --> L[Allow: Allowed Domains]
    G --> M[Deny: All Other Networks]
    
    H --> N[CPU/Memory Limits]
    H --> O[Process Tree Isolation]
    
    I --> P[Sandboxed Process Spawn]
    P --> Q[Output Capture]
    Q --> R[Violation Annotation]

沙箱启用决策树

function shouldUseSandbox(input: SandboxInput): boolean {
  // 1. 检查全局沙箱开关
  if (!SandboxManager.isSandboxingEnabled()) return false
  
  // 2. 检查显式禁用标志(需要策略允许)
  if (input.dangerouslyDisableSandbox && 
      SandboxManager.areUnsandboxedCommandsAllowed()) {
    return false
  }
  
  // 3. 检查命令是否存在
  if (!input.command) return false
  
  // 4. 检查用户配置的排除命令
  if (containsExcludedCommand(input.command)) return false
  
  return true
}

Sources: shouldUseSandbox.ts, sandbox-adapter.ts

文件系统隔离

沙箱通过路径白名单和黑名单控制文件访问:

路径解析规则

  • //path → 绝对路径(从文件系统根开始)
  • /path → 相对于设置文件目录
  • ~/path → 用户主目录
  • ./pathpath → 当前工作目录

文件系统配置转换

// 从 Claude Code 设置转换为 SandboxRuntime 配置
function convertToSandboxRuntimeConfig(settings: SettingsJson) {
  // 1. 提取网络域名白名单
  const allowedDomains = extractAllowedDomains(settings)
  
  // 2. 构建文件系统规则
  const fsConfig = {
    allowRead: resolveReadPaths(settings.permissions.allow),
    denyRead: resolveReadPaths(settings.permissions.deny),
    allowWrite: resolveWritePaths(settings.sandbox.filesystem.allowWrite),
    denyWrite: resolveWritePaths(settings.sandbox.filesystem.denyWrite),
  }
  
  // 3. 应用策略设置覆盖
  if (shouldAllowManagedReadPathsOnly()) {
    fsConfig.allowRead = getPolicyReadPaths()
  }
  
  return { network: { allowedDomains }, filesystem: fsConfig }
}

临时目录隔离: 每个用户拥有独立的临时目录,防止多用户权限冲突:

const sandboxTmpDir = posixJoin(
  process.env.CLAUDE_CODE_TMPDIR || '/tmp',
  getClaudeTempDirName(),  // 用户特定的目录名
)
await fs.mkdir(sandboxTmpDir, { mode: 0o700 })  // 仅所有者可访问

Sources: sandbox-adapter.ts, sandbox-adapter.ts, Shell.ts

网络隔离

沙箱限制进程的网络访问,只允许白名单域名:

网络域名提取

// 从 WebFetch 权限规则提取域名
for (const ruleString of settings.permissions.allow) {
  const rule = permissionRuleValueFromString(ruleString)
  if (rule.toolName === 'WebFetch' && 
      rule.ruleContent?.startsWith('domain:')) {
    allowedDomains.push(rule.ruleContent.substring('domain:'.length))
  }
}

策略模式: 当启用 allowManagedDomainsOnly 时,只使用策略设置中的域名,忽略用户配置:

if (shouldAllowManagedSandboxDomainsOnly()) {
  // 仅使用 policySettings.sandbox.network.allowedDomains
  const policySettings = getSettingsForSource('policySettings')
  allowedDomains = policySettings?.sandbox?.network?.allowedDomains || []
}

Sources: sandbox-adapter.ts

沙箱违规处理

当沙箱进程违反限制时,系统会捕获违规事件并标注输出:

// 在输出中标注沙箱违规
const outputWithSbFailures = SandboxManager.annotateStderrWithSandboxFailures(
  input.command, 
  result.stdout || ''
)

违规事件类型

  • 文件系统访问违规(读写被拒绝的路径)
  • 网络连接违规(访问非白名单域名)
  • 资源限制违规(CPU/内存超限)

Bare Git Repo 清理: Linux 上的 Bubblewrap 会在工作目录创建 0 字节的挂载点文件(如 .bashrcHEAD),这些”幽灵文件”在沙箱退出后仍然存在。系统会在命令执行后同步清理:

void shellCommand.result.then(async result => {
  // 同步清理幽灵文件,确保调用者在同一微任务中看到干净的工作树
  if (shouldUseSandbox) {
    SandboxManager.cleanupAfterCommand()
  }
  // ... 后续处理
})

Sources: BashTool.tsx, sandbox-adapter.ts, Shell.ts

权限系统:细粒度访问控制

权限决策流程

BashTool 的权限系统基于规则引擎分类器双重机制:

flowchart TD
    A[Command Input] --> B{Check Rules}
    B -->|Exact Match| C{Allow Rule?}
    B -->|Prefix Match| D{Allow Prefix?}
    B -->|No Match| E{Classifier Enabled?}
    
    C -->|Yes| F[Allow]
    C -->|No| G[Deny]
    
    D -->|Yes| F
    D -->|No| G
    
    E -->|Yes| H[ML Classifier]
    E -->|No| I[Ask User]
    
    H -->|High Confidence| J{Allow/Deny}
    H -->|Low Confidence| I
    
    J -->|Allow| F
    J -->|Deny| G
    
    F --> K[Execute Command]
    G --> L[Block Command]
    I --> M[Show Permission Dialog]

规则匹配类型

  1. 精确匹配Bash(git status) → 仅匹配 git status
  2. 前缀匹配Bash(npm run:*) → 匹配所有 npm run 子命令
  3. 通配符匹配Bash(docker *) → 匹配所有 docker 开头的命令

Sources: bashPermissions.ts

命令前缀提取

对于多子命令工具(如 git commitnpm run),系统会智能提取稳定的前缀:

function getSimpleCommandPrefix(command: string): string | null {
  const tokens = command.trim().split(/\s+/)
  
  // 跳过安全的环境变量赋值
  let i = 0
  while (i < tokens.length && ENV_VAR_ASSIGN_RE.test(tokens[i])) {
    const varName = tokens[i].split('=')[0]
    if (!SAFE_ENV_VARS.has(varName)) {
      return null  // 非安全变量,回退到精确匹配
    }
    i++
  }
  
  const remaining = tokens.slice(i)
  if (remaining.length < 2) return null
  
  // 第二个 token 必须看起来像子命令(小写字母开头)
  const subcmd = remaining[1]
  if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(subcmd)) {
    return null
  }
  
  return remaining.slice(0, 2).join(' ')  // "git commit"
}

示例转换

  • git commit -m "fix"git commit
  • NODE_ENV=prod npm run buildnpm runNODE_ENV 在安全列表中)
  • MY_VAR=val npm run buildnullMY_VAR 不安全,回退到精确匹配)

Sources: bashPermissions.ts

复合命令拆分

对于 &&||; 连接的复合命令,系统会拆分为子命令并逐个验证:

// 拆分复合命令
const subcommands = splitCommand_DEPRECATED(command)

// 为每个子命令检查权限
for (const subcommand of subcommands) {
  const result = checkSingleCommand(subcommand)
  if (result.behavior === 'deny') {
    return result  // 任一子命令被拒绝,整个命令被拒绝
  }
}

安全限制: 为防止拒绝服务攻击,系统限制最多检查 50 个子命令:

export const MAX_SUBCOMMANDS_FOR_SECURITY_CHECK = 50

超过限制的命令会回退到 ask 行为(安全默认)。

Sources: bashPermissions.ts

机器学习分类器

BashTool 集成了可选的 ML 分类器,用于自动判断命令安全性:

分类器输出

type ClassifierResult = {
  behavior: 'allow' | 'deny' | 'ask'
  confidence: number  // 0.0 - 1.0
  matchedDescription?: string
  reason?: string
}

分类器决策逻辑

  • 高置信度(> 0.9):自动允许或拒绝
  • 低置信度(≤ 0.9):询问用户

分类器仅作为辅助决策,最终的权限规则优先级更高。如果规则明确允许,即使分类器认为危险也会执行。

Sources: bashPermissions.ts

只读命令识别:优化并发执行

只读命令白名单

BashTool 维护了详细的只读命令白名单,用于:

  1. 判断命令是否可安全并发执行
  2. 折叠显示搜索/读取类命令的输出
  3. 自动批准低风险操作

命令分类

类别命令示例用途
搜索命令grep, rg, find, ag文本搜索、文件查找
读取命令cat, head, tail, less查看文件内容
列表命令ls, tree, du目录浏览
分析命令wc, stat, file, jq文件分析、数据处理
语义中性echo, printf, true, false纯输出/状态命令

管道验证: 对于管道命令 cmd1 | cmd2 | cmd3所有部分都必须是只读命令,整体才被视为只读:

export function isSearchOrReadBashCommand(command: string) {
  const partsWithOperators = splitCommandWithOperators(command)
  
  for (const part of partsWithOperators) {
    // 跳过操作符和重定向
    if (['||', '&&', '|', ';', '>', '>>'].includes(part)) continue
    
    const baseCommand = part.trim().split(/\s+/)[0]
    
    // 语义中性命令不影响整体性质
    if (BASH_SEMANTIC_NEUTRAL_COMMANDS.has(baseCommand)) continue
    
    // 任一非只读命令导致整体为非只读
    if (!isReadOnlyCommand(baseCommand)) {
      return { isSearch: false, isRead: false, isList: false }
    }
  }
  
  return { isSearch: true, isRead: true, isList: true }
}

Sources: BashTool.tsx, readOnlyValidation.ts

Flag 参数验证

许多命令的读写性质取决于 flag 参数。例如 git push 是写操作,但 git push --dry-run 是只读操作。

安全 Flag 白名单

const FD_SAFE_FLAGS: Record<string, FlagArgType> = {
  '-h': 'none',           // 帮助
  '--hidden': 'none',     // 显示隐藏文件
  '-t': 'string',         // 文件类型过滤
  '--exclude': 'string',  // 排除模式
  // 注意:-x/--exec 被排除,因为会执行任意命令
}

Flag 验证流程

function validateFlags(
  command: string,
  args: string[],
  config: CommandConfig
): boolean {
  for (let i = 0; i < args.length; i++) {
    const arg = args[i]
    
    // 遇到 -- 停止解析(大多数工具)
    if (arg === '--' && config.respectsDoubleDash !== false) {
      break
    }
    
    // 检查是否在白名单中
    const flagType = config.safeFlags[arg]
    if (!flagType) {
      return false  // 未知 flag,拒绝
    }
    
    // 消费参数(如果有)
    if (flagType !== 'none') {
      i++  // 跳过参数值
    }
  }
  
  return true
}

安全陷阱示例

# ❌ 危险:-x 会执行任意命令
find . -name "*.sh" -x rm {}

# ✅ 安全:仅列出文件
find . -name "*.sh"

Sources: readOnlyValidation.ts

Git 命令特殊处理

Git 命令有特殊的读写语义,系统维护了详细的只读子命令列表:

只读 Git 命令

  • git status, git log, git diff, git show
  • git branch --list, git tag --list
  • git remote --verbose, git config --list

写操作 Git 命令

  • git commit, git push, git reset
  • git checkout, git merge, git rebase

边界情况

# ✅ 只读:--dry-run 不修改任何内容
git push --dry-run

# ❌ 写操作:会修改工作树
git checkout -- .

Sources: readOnlyValidation.ts

命令执行流水线

执行器架构

BashTool 的执行层由多个协作组件构成:

sequenceDiagram
    participant BT as BashTool
    participant SE as Shell Executor
    participant SB as Sandbox Manager
    participant SP as Shell Provider
    participant TO as TaskOutput
    
    BT->>SE: exec(command, options)
    SE->>SP: findSuitableShell()
    SP-->>SE: shellPath, provider
    
    alt Sandbox Enabled
        SE->>SB: shouldUseSandbox(input)
        SB-->>SE: true
        SE->>SB: wrapWithSandbox(cmd, shell)
        SB->>SB: Build Config
        SB-->>SE: sandboxedCommand
    end
    
    SE->>TO: Create TaskOutput
    SE->>SP: buildExecCommand(cmd, sandboxConfig)
    SP-->>SE: commandString, cwdFilePath
    
    SE->>SP: spawn(binary, args, options)
    SP-->>SE: ChildProcess
    
    loop Progress Updates
        SP-->>TO: stdout/stderr chunks
        TO-->>BT: onProgress callback
        BT-->>BT: Yield progress event
    end
    
    SP-->>SE: Exit Code
    SE->>TO: Read final output
    TO-->>SE: stdout, stderr
    SE-->>BT: ExecResult

Shell Provider 接口

interface ShellProvider {
  shellPath: string                    // Shell 二进制路径
  detached: boolean                    // 是否分离进程
  
  buildExecCommand(command, options): {
    commandString: string
    cwdFilePath: string
  }
  
  getSpawnArgs(commandString): string[]
  getEnvironmentOverrides(command): Record<string, string>
}

内置 Provider

  • BashProvider:标准 Bash/Zsh 执行
  • PowerShellProvider:Windows PowerShell 执行(Base64 编码命令)

Sources: Shell.ts, Shell.ts

工作目录恢复

命令执行可能导致工作目录变化(cd 命令),系统通过临时文件跟踪 CWD:

CWD 跟踪机制

// Shell Provider 构建命令时会注入 pwd 追踪
const builtCommand = `
  ${userCommand}
  pwd -P >| "${cwdFilePath}"
`

// 命令完成后读取新的 CWD
void shellCommand.result.then(async result => {
  if (!preventCwdChanges && !result.backgroundTaskId) {
    const newCwd = readFileSync(nativeCwdFilePath, 'utf8').trim()
    
    // 验证新目录存在
    await realpath(newCwd)
    setCwdState(newCwd)
    
    // 触发 Hook 系统通知
    onCwdChangedForHooks(newCwd)
  }
})

目录恢复逻辑: 如果当前 CWD 被删除(例如临时目录清理),系统会回退到原始启动目录:

try {
  await realpath(cwd)
} catch {
  const fallback = getOriginalCwd()
  logForDebugging(`Shell CWD "${cwd}" no longer exists, recovering to "${fallback}"`)
  setCwdState(fallback)
}

Sources: Shell.ts

进度流式传输

长时间运行的命令会实时流式传输输出,提供进度反馈:

进度信号机制

// 创建进度唤醒信号
let resolveProgress: (() => void) | null = null
function createProgressSignal(): Promise<null> {
  return new Promise(resolve => {
    resolveProgress = () => resolve(null)
  })
}

// Shell 执行器的进度回调
const shellCommand = await exec(command, signal, 'bash', {
  timeout: timeoutMs,
  onProgress(lastLines, allLines, totalLines, totalBytes, isIncomplete) {
    lastProgressOutput = lastLines
    fullOutput = allLines
    
    // 唤醒 generator 以 yield 进度更新
    if (resolveProgress) {
      resolveProgress()
      resolveProgress = null
    }
  },
  shouldUseSandbox: shouldUseSandbox(input),
  shouldAutoBackground: autoBackgroundingEnabled
})

// Generator 循环
while (true) {
  await createProgressSignal()
  yield {
    type: 'progress',
    output: lastProgressOutput,
    fullOutput: fullOutput,
    elapsedTimeSeconds: elapsedTime,
    totalLines: lastTotalLines,
    totalBytes: lastTotalBytes
  }
}

自动后台化: 对于 Assistant 模式,长时间运行的阻塞命令会自动移至后台:

const ASSISTANT_BLOCKING_BUDGET_MS = 15_000  // 15 秒后自动后台化

if (shouldAutoBackground && elapsedTime > ASSISTANT_BLOCKING_BUDGET_MS) {
  const backgroundTaskId = await moveTaskToBackground(shellCommand)
  return {
    ...result,
    backgroundTaskId,
    assistantAutoBackgrounded: true
  }
}

Sources: BashTool.tsx

输出处理与持久化

大型输出(> 30KB)会被持久化到磁盘,避免内存溢出:

输出持久化流程

const MAX_PERSISTED_SIZE = 64 * 1024 * 1024  // 64 MB

if (result.outputFilePath && result.outputTaskId) {
  const fileStat = await fsStat(result.outputFilePath)
  persistedOutputSize = fileStat.size
  
  // 截断超大文件
  if (fileStat.size > MAX_PERSISTED_SIZE) {
    await fsTruncate(result.outputFilePath, MAX_PERSISTED_SIZE)
  }
  
  // 复制到工具结果目录
  const dest = getToolResultPath(result.outputTaskId, false)
  await link(result.outputFilePath, dest)  // 或 copyFile
  
  persistedOutputPath = dest
}

// 为模型构建持久化输出消息
if (persistedOutputPath) {
  const preview = generatePreview(stdout, PREVIEW_SIZE_BYTES)
  processedStdout = buildLargeToolResultMessage({
    filepath: persistedOutputPath,
    originalSize: persistedOutputSize,
    preview: preview.preview,
    hasMore: preview.hasMore
  })
}

图像输出处理: 命令输出可能包含图像(例如终端截图),系统会自动检测并压缩:

let isImage = isImageOutput(stdout)

if (isImage) {
  const resized = await resizeShellImageOutput(stdout, result.outputFilePath)
  if (resized) {
    compressedStdout = resized
  } else {
    isImage = false  // 压缩失败,作为文本处理
  }
}

Sources: BashTool.tsx

命令语义解释

退出码语义映射

不同命令对退出码有不同的语义解释。BashTool 维护了命令特定的语义映射表:

语义映射配置

const COMMAND_SEMANTICS: Map<string, CommandSemantic> = new Map([
  // grep: 0=匹配, 1=无匹配, 2+=错误
  ['grep', (exitCode) => ({
    isError: exitCode >= 2,
    message: exitCode === 1 ? 'No matches found' : undefined
  })],
  
  // diff: 0=无差异, 1=有差异, 2+=错误
  ['diff', (exitCode) => ({
    isError: exitCode >= 2,
    message: exitCode === 1 ? 'Files differ' : undefined
  })],
  
  // find: 0=成功, 1=部分成功, 2+=错误
  ['find', (exitCode) => ({
    isError: exitCode >= 2,
    message: exitCode === 1 ? 'Some directories were inaccessible' : undefined
  })],
  
  // test/[: 0=真, 1=假, 2+=错误
  ['test', (exitCode) => ({
    isError: exitCode >= 2,
    message: exitCode === 1 ? 'Condition is false' : undefined
  })]
])

语义解释流程

function interpretCommandResult(
  command: string,
  exitCode: number,
  stdout: string,
  stderr: string
): { isError: boolean; message?: string } {
  const semantic = getCommandSemantic(command)
  return semantic(exitCode, stdout, stderr)
}

错误抛出逻辑: 如果命令被解释为错误,会抛出 ShellError

if (interpretationResult.isError && !isInterrupt) {
  throw new ShellError('', outputWithSbFailures, result.code, result.interrupted)
}

Sources: commandSemantics.ts, BashTool.tsx

破坏性命令警告

系统会检测潜在破坏性命令并显示警告(但不阻止执行):

破坏性模式检测

const DESTRUCTIVE_PATTERNS: DestructivePattern[] = [
  // Git 数据丢失
  { pattern: /\bgit\s+reset\s+--hard\b/,
    warning: 'Note: may discard uncommitted changes' },
  { pattern: /\bgit\s+push\b[^;&|\n]*[ \t](--force|--force-with-lease|-f)\b/,
    warning: 'Note: may overwrite remote history' },
  { pattern: /\bgit\s+clean\b(?![^;&|\n]*(?:-[a-zA-Z]*n|--dry-run))[^;&|\n]*-[a-zA-Z]*f/,
    warning: 'Note: may permanently delete untracked files' },
  
  // 文件删除
  { pattern: /(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*[rR][a-zA-Z]*f/,
    warning: 'Note: may recursively force-remove files' },
  
  // 数据库操作
  { pattern: /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA)\b/i,
    warning: 'Note: may drop or truncate database objects' },
  
  // 基础设施
  { pattern: /\bkubectl\s+delete\b/,
    warning: 'Note: may delete Kubernetes resources' },
  { pattern: /\bterraform\s+destroy\b/,
    warning: 'Note: may destroy Terraform infrastructure' }
]

警告信息会显示在权限对话框中,提醒用户注意风险。

Sources: destructiveCommandWarning.ts

Sed 内联编辑特殊处理

BashTool 能够识别 sed -i 内联编辑命令,并将其转换为等效的文件编辑操作:

Sed 命令解析

function parseSedEditCommand(command: string): SedEditInfo | null {
  // 匹配模式:sed -i 's/old/new/' file.txt
  const match = command.match(/^sed\s+-i\s+'([^']+)'?\s+(.+)$/)
  if (!match) return null
  
  const [, sedExpression, filePath] = match
  
  // 解析 sed 表达式:s/old/new/
  const sedMatch = sedExpression.match(/^s(.)(.+?)\1(.+?)\1(g?)$/)
  if (!sedMatch) return null
  
  const [, , oldString, newString, globalFlag] = sedMatch
  
  return {
    filePath,
    oldString,
    newString,
    isGlobal: globalFlag === 'g'
  }
}

转换为文件编辑

// 在 validateInput 中检测 sed 命令
const sedInfo = parseSedEditCommand(input.command)
if (sedInfo) {
  // 读取文件内容
  const originalContent = await readFile(sedInfo.filePath, 'utf8')
  
  // 执行替换
  const newContent = sedInfo.isGlobal
    ? originalContent.replaceAll(sedInfo.oldString, sedInfo.newString)
    : originalContent.replace(sedInfo.oldString, sedInfo.newString)
  
  // 写入文件
  await writeTextContent(sedInfo.filePath, newContent)
  
  // 通知 VS Code
  notifyVscodeFileUpdated(sedInfo.filePath, originalContent, newContent)
  
  return { data: { stdout: '', stderr: '', interrupted: false } }
}

这种转换使得 sed -i 命令在 UI 中显示为文件编辑操作,而非普通 Bash 命令。

Sources: BashTool.tsx, sedEditParser.ts, sedValidation.ts

Claude Code Hints 协议

BashTool 实现了零令牌的插件推荐协议:CLI/SDK 通过 CLAUDECODE=1 环境变量标识,并在 stderr 中输出 <claude-code-hint /> 标签。

Hint 提取流程

// 扫描输出中的 hint 标签
const extracted = extractClaudeCodeHints(strippedStdout, input.command)
strippedStdout = extracted.stripped  // 从输出中移除标签

// 仅在主线程记录 hint(避免子代理污染)
if (isMainThread && extracted.hints.length > 0) {
  for (const hint of extracted.hints) {
    maybeRecordPluginHint(hint)  // 记录以供推荐系统使用
  }
}

Hint 标签格式

<claude-code-hint 
  plugin-name="my-lsp" 
  install-command="npm install -g my-lsp"
  reason="Provides better code intelligence for TypeScript"
/>

这个协议允许外部工具在不消耗模型令牌的情况下推荐插件安装。

Sources: BashTool.tsx

总结与最佳实践

BashTool 是 Claude Code 安全架构的核心组件,通过多层防御机制确保命令执行的安全性:

关键设计原则

  1. 深度防御:AST 解析 → 安全验证 → 权限检查 → 沙箱隔离
  2. 最小权限:只读命令自动识别,沙箱默认拒绝所有访问
  3. 显式授权:未知命令询问用户,危险操作显示警告
  4. 零信任:所有输入都经过严格验证,不信任任何外部数据

配置建议

  • 启用沙箱:sandbox.enabled: true
  • 配置排除命令:sandbox.excludedCommands: ["bazel", "npm run"]
  • 设置网络白名单:sandbox.network.allowedDomains: ["api.github.com"]
  • 使用策略设置:policySettings 覆盖用户配置

安全注意事项

  • 沙箱不是万能的:有经验的攻击者可能绕过限制
  • 定期审查权限规则:移除不再需要的允许规则
  • 监控沙箱违规:关注 SandboxViolation 事件
  • 使用 failIfUnavailable:确保沙箱可用时才执行命令

BashTool 的设计体现了”安全默认 + 灵活配置”的理念,在保护用户的同时保持易用性。通过理解其内部机制,开发者可以更好地利用这些安全特性,构建可靠的自动化工作流。