BashTool 与 PowerShellTool:Shell 执行器

📑 目录

在 Claude Code 的工具体系中,Shell 执行器是最为核心且使用频率最高的组件之一。无论是代码编译、文件检索、版本控制,还是系统诊断,AI Agent 都需要通过 Shell 命令与宿主系统交互。Claude Code 为此设计了两套并行的 Shell 执行工具:BashTool 面向 POSIX 环境(Linux / macOS / WSL),PowerShellTool 则专为 Windows 原生平台打造。本文将从源码层面深入解析这两者的架构设计、执行流程、环境管理、输出控制以及安全策略。

一、BashTool 架构概览

1.1 文件位置与模块职责

BashTool 的实现位于 src/tools/BashTool/BashTool.tsx,这是一个典型的 React + TypeScript 模块。它并非单纯的函数集合,而是以 buildTool 工厂函数构建的完整 Tool 定义对象,集成了权限校验、输入校验、UI 渲染、结果映射等生命周期钩子。

// src/tools/BashTool/BashTool.tsx (第 1-30 行)
import { feature } from 'bun:bundle';
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
import { copyFile, stat as fsStat, truncate as fsTruncate, link } from 'fs/promises';
import * as React from 'react';
import type { CanUseToolFn } from 'src/hooks/useCanUseTool.js';
import type { AppState } from 'src/state/AppState.js';
import { z } from 'zod/v4';
// ... 其他导入

BashTool 的导入清单极为庞大,涵盖了从文件系统操作、分析日志、沙箱适配到权限系统的方方面面。这种高度模块化的设计让每个关注点都能独立演化:权限规则存于 bashPermissions.ts,安全语义解析在 bashSecurity.ts,沙箱决策由 shouldUseSandbox.ts 负责,UI 组件则分散在 UI.tsxutils.tsx 中。

1.2 输入参数定义

BashTool 的输入参数通过 Zod schema 进行运行时校验。核心字段包括:

字段类型说明
commandstring要执行的 Bash 命令(必填)
timeoutnumber(可选)超时时间(毫秒),上限由 getMaxTimeoutMs() 决定
descriptionstring(可选)命令描述,用于 UI 展示
run_in_backgroundboolean(可选)是否在后台运行
dangerouslyDisableSandboxboolean(可选)是否强制绕过沙箱
_simulatedSedEditobject(可选)内部字段:sed 编辑的预计算结果
// src/tools/BashTool/BashTool.tsx (第 216-245 行)
const fullInputSchema = lazySchema(() => z.strictObject({
  command: z.string().describe('The command to execute'),
  timeout: semanticNumber(z.number().optional()).describe(
    `Optional timeout in milliseconds (max ${getMaxTimeoutMs()})`
  ),
  description: z.string().optional().describe(
    `Clear, concise description of what this command does...`
  ),
  run_in_background: semanticBoolean(z.boolean().optional()).describe(
    `Set to true to run this command in the background...`
  ),
  dangerouslyDisableSandbox: semanticBoolean(z.boolean().optional()).describe(
    'Set this to true to dangerously override sandbox mode...'
  ),
  _simulatedSedEdit: z.object({
    filePath: z.string(),
    newContent: z.string()
  }).optional().describe('Internal: pre-computed sed edit result from preview')
}));

值得注意的是 _simulatedSedEdit 字段被显式标注为 Internal,并且在暴露给模型的 schema 中被剔除。这是因为该字段绕过了权限检查与沙箱限制,若被模型直接利用,将构成严重的安全漏洞。

1.3 执行流程

BashTool 的核心执行逻辑在 call 方法中,但它并不直接执行命令,而是委托给 runShellCommand 这一异步生成器函数:

flowchart TD
    A[用户输入 command] --> B[validateInput]
    B --> C{检查 sleep 阻塞模式?}
    C -->|是| D[返回错误: 使用 Monitor 或后台运行]
    C -->|否| E[checkPermissions]
    E --> F{权限通过?}
    F -->|否| G[请求用户确认]
    F -->|是| H[runShellCommand 生成器]
    H --> I[exec 创建子进程]
    I --> J{run_in_background?}
    J -->|是| K[spawnBackgroundTask]
    J -->|否| L[前台轮询输出]
    L --> M{超时或中断?}
    M -->|超时| N[autoBackground 或 kill]
    M -->|中断| O[background 或 kill]
    M -->|完成| P[结果处理与返回]
    N --> P
    O --> P
    K --> P

runShellCommand 的设计颇具匠心。它采用 AsyncGenerator 模式,在命令执行期间持续 yield 进度信息,使得 UI 层可以实时更新输出,而不会阻塞主线程:

// src/tools/BashTool/BashTool.tsx (第 1013-1030 行)
async function* runShellCommand({
  input, abortController, setAppState, setToolJSX,
  preventCwdChanges, isMainThread, toolUseId, agentId
}): AsyncGenerator<Progress, ExecResult, void> {
  const { command, timeout, run_in_background } = input;
  const timeoutMs = timeout || getDefaultTimeoutMs();
  // ...
  const shellCommand = await exec(command, abortController.signal, 'bash', {
    timeout: timeoutMs,
    onProgress(lastLines, allLines, totalLines, totalBytes, isIncomplete) {
      // 唤醒生成器,yield 最新进度
    },
    preventCwdChanges,
    shouldUseSandbox: shouldUseSandbox(input),
    shouldAutoBackground
  });
  // ...
}

这里的 exec 函数来自 src/utils/Shell.ts,它是所有 Shell 执行的统一入口,负责进程创建、环境变量注入、工作目录恢复等底层操作。我们将在后文详述。

二、PowerShellTool 架构

2.1 文件位置与模块复用

PowerShellTool 位于 src/tools/PowerShellTool/PowerShellTool.tsx,其整体结构与 BashTool 高度相似——同样是 buildTool 工厂的产物,同样拥有 callvalidateInputcheckPermissionsmapToolResultToToolResultBlockParam 等生命周期方法。但它在多处复用了 BashTool 的共享逻辑,例如 shouldUseSandboxBackgroundHintbuildImageToolResult 等:

// src/tools/PowerShellTool/PowerShellTool.tsx (第 1-30 行)
import { shouldUseSandbox } from '../BashTool/shouldUseSandbox.js';
import { BackgroundHint } from '../BashTool/UI.js';
import {
  buildImageToolResult, isImageOutput, resetCwdIfOutsideProject,
  resizeShellImageOutput, stdErrAppendShellResetMessage, stripEmptyLines
} from '../BashTool/utils.js';

这种复用策略降低了跨平台维护成本。当安全策略、输出处理或 UI 行为需要调整时,只需修改一处即可同时影响两个工具。

2.2 与 BashTool 的核心差异

尽管结构相似,PowerShellTool 在语义层和平台层存在显著差异:

维度BashToolPowerShellTool
Shell 类型bash / zshpwsh / powershell
命令分类grep, find, cat, lsSelect-String, Get-Content, Get-ChildItem
语义中性命令echo, printf, true, falseWrite-Output, Write-Host
静默命令mv, cp, rm, touch(无对应概念,依赖 PS 行为)
退出码语义$? + $LASTEXITCODE 混合$LASTEXITCODE 优先,其次 $?
编码方式直接字符串传递Base64 UTF-16LE (-EncodedCommand)

其中,命令分类的差异尤为关键。Claude Code 的 UI 会将搜索/读取类命令(如 grepSelect-String)折叠显示,避免长输出淹没对话历史。PowerShellTool 为此维护了一套独立的命令白名单:

// src/tools/PowerShellTool/PowerShellTool.tsx (第 48-85 行)
const PS_SEARCH_COMMANDS = new Set([
  'select-string',      // grep 等效
  'get-childitem',      // find 等效(带 -Recurse)
  'findstr',            // Windows 原生搜索
  'where.exe'           // which 等效
]);

const PS_READ_COMMANDS = new Set([
  'get-content',        // cat 等效
  'get-item',           // file 信息
  'test-path',          // test -e 等效
  'resolve-path',       // realpath 等效
  'get-process',        // ps 等效
  'get-service',        // 系统信息
  // ... 更多 cmdlet
]);

2.3 Windows 平台适配

PowerShellTool 最大的平台适配挑战在于沙箱不可用。Claude Code 的沙箱依赖 bwrap(Linux)或 sandbox-exec(macOS),这些工具在 Windows 原生环境中不存在。因此代码中设置了明确的策略拒绝:

// src/tools/PowerShellTool/PowerShellTool.tsx (第 183-196 行)
const WINDOWS_SANDBOX_POLICY_REFUSAL = 
  'Enterprise policy requires sandboxing, but sandboxing is not available ' +
  'on native Windows. Shell command execution is blocked on this platform by policy.';

function isWindowsSandboxPolicyViolation(): boolean {
  return getPlatform() === 'windows' 
    && SandboxManager.isSandboxEnabledInSettings() 
    && !SandboxManager.areUnsandboxedCommandsAllowed();
}

这意味着:如果企业策略强制要求沙箱,而用户又在原生 Windows 上运行,PowerShellTool 将直接拒绝执行。这是防御性设计的典范——宁可不可用,也不冒险在无沙箱环境下运行敏感命令。

另一个平台适配点是命令编码。由于沙箱运行时会额外施加一层 shellquote.quote(),PowerShell 中的特殊字符(如 !)会被错误转义。解决方案是采用 -EncodedCommand 参数,将命令编码为 Base64 UTF-16LE:

// src/utils/shell/powershellProvider.ts (第 22-27 行)
function encodePowerShellCommand(psCommand: string): string {
  return Buffer.from(psCommand, 'utf16le').toString('base64');
}

在沙箱模式下,完整的调用链变成:

bwrap ... /bin/sh -c 'pwsh -NoProfile -NonInteractive -EncodedCommand SGVsbG8gV29ybGQ=...'

Base64 字符集 [A-Za-z0-9+/=] 不含任何需要转义的符号,因此可以安全地穿越多层引号包装。

三、环境变量管理

3.1 当前工作目录(CWD)

Shell 命令的执行环境高度依赖于当前工作目录。Claude Code 通过一个精巧的文件机制来跟踪 CWD 变化:每次执行命令前,会在临时目录创建一个 cwd 标记文件;命令执行完毕后,读取该文件内容即可获知命令结束时的实际工作目录。

对于 Bash,这一机制在 bashProvider.ts 中实现:

// src/utils/shell/bashProvider.ts (第 95-110 行)
const shellCwdFilePath = opts.useSandbox
  ? posixJoin(opts.sandboxTmpDir!, `cwd-${opts.id}`)
  : posixJoin(shellTmpdir, `claude-${opts.id}-cwd`);

// 在构建的命令字符串末尾追加:
// pwd -P >| /tmp/claude-xxx-cwd

对于 PowerShell,逻辑类似但语法不同:

// src/utils/shell/powershellProvider.ts (第 55-65 行)
const cwdTracking = `
; $_ec = if ($null -ne $LASTEXITCODE) { $LASTEXITCODE } elseif ($?) { 0 } else { 1 }
; (Get-Location).Path | Out-File -FilePath '${escapedCwdFilePath}' -Encoding utf8 -NoNewline
; exit $_ec`;
const psCommand = command + cwdTracking;

这种设计允许命令内部执行 cdSet-Location 后,Claude Code 仍能准确掌握新的工作目录,从而保证后续命令在正确的路径下执行。

3.2 环境变量继承

子进程的环境变量通过 subprocessEnv() 获取基线,再叠加特定覆盖:

// src/utils/Shell.ts (第 195-215 行)
const childProcess = spawn(spawnBinary, shellArgs, {
  env: {
    ...subprocessEnv(),
    SHELL: shellType === 'bash' ? binShell : undefined,
    GIT_EDITOR: 'true',
    CLAUDECODE: '1',
    ...envOverrides,
    ...(process.env.USER_TYPE === 'ant'
      ? { CLAUDE_CODE_SESSION_ID: getSessionId() }
      : {}),
  },
  cwd,
  stdio: usePipeMode ? ['pipe', 'pipe', 'pipe'] : ['pipe', outputHandle?.fd, outputHandle?.fd],
  detached: provider.detached,
  windowsHide: true,
});

其中几个关键环境变量:

  • GIT_EDITOR: 'true':将 git 的交互式编辑器设为一个总是成功的空操作,防止 git commit 等命令因等待编辑器输入而挂起。
  • CLAUDECODE: '1':向外部 CLI 工具表明当前运行在 Claude Code 环境中。部分工具会据此输出 <claude-code-hint /> 标签,提示可用插件或快捷操作。
  • SHELL:仅对 Bash 设置,指向实际使用的 shell 路径(bash 或 zsh)。

3.3 自定义环境变量注入

PowerShellTool 特别处理了会话级环境变量的注入。Claude Code 支持通过 /env 命令设置环境变量,这些变量需要传递给子进程:

// src/utils/shell/powershellProvider.ts (第 88-102 行)
async getEnvironmentOverrides(): Promise<Record<string, string>> {
  const env: Record<string, string> = {};
  // 先应用会话变量,确保用户设置生效
  for (const [key, value] of getSessionEnvVars()) {
    env[key] = value;
  }
  if (currentSandboxTmpDir) {
    // 后应用沙箱 TMPDIR,防止用户意外覆盖
    env.TMPDIR = currentSandboxTmpDir;
    env.CLAUDE_CODE_TMPDIR = currentSandboxTmpDir;
  }
  return env;
}

这里存在微妙的优先级设计:用户通过 /env 设置的变量先写入,但沙箱相关的 TMPDIR 后写入并覆盖前者。这确保了沙箱隔离不被用户配置意外破坏。

四、输出处理

4.1 stdout / stderr 捕获

Claude Code 提供两种输出捕获模式:文件模式(默认)和管道模式(用于 hooks)。

在文件模式下,stdout 和 stderr 被重定向到同一个文件描述符:

// src/utils/Shell.ts (第 220-235 行)
const outputHandle = await open(
  taskOutput.path,
  process.platform === 'win32'
    ? 'w'
    : fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_APPEND | O_NOFOLLOW,
);

在 POSIX 系统上,O_APPEND 保证每次写入都是原子的(先 seek 到末尾再写),因此 stdout 和 stderr 可以按时间顺序交错,而不会出现内容撕裂。在 Windows 上,由于 MSYS2/Cygwin 的兼容性问题,使用 'w' 模式而非 O_APPEND,但利用 FILE_SYNCHRONOUS_IO_NONALERT 的内核级锁保证串行化。

在管道模式下(如 hooks 使用),则通过 StreamWrapper 类实时将数据流导入内存中的 TaskOutput

// src/utils/ShellCommand.ts (第 55-85 行)
class StreamWrapper {
  #stream: Readable | null;
  #taskOutput: TaskOutput | null;
  #isStderr: boolean;

  constructor(stream: Readable, taskOutput: TaskOutput, isStderr: boolean) {
    this.#stream = stream;
    this.#taskOutput = taskOutput;
    this.#isStderr = isStderr;
    stream.setEncoding('utf-8');
    stream.on('data', this.#onData);
  }

  #dataHandler(data: Buffer | string): void {
    const str = typeof data === 'string' ? data : data.toString();
    if (this.#isStderr) {
      this.#taskOutput!.writeStderr(str);
    } else {
      this.#taskOutput!.writeStdout(str);
    }
  }
}

4.2 输出截断与持久化

AI Agent 的上下文窗口有限,海量输出会直接挤占宝贵的 token 预算。Claude Code 为此设计了多层截断机制:

flowchart LR
    A[命令输出] --> B{是否超过 MAX_TASK_OUTPUT_BYTES?}
    B -->|是| C[写入磁盘文件]
    B -->|否| D[保留在内存]
    C --> E{是否超过 64 MB?}
    E -->|是| F[truncate 到 64 MB]
    E -->|否| G[完整保留]
    F --> H[link/copy 到 tool-results 目录]
    G --> H
    H --> I[模型通过 FileRead 访问]
    D --> J[直接返回模型]

具体实现中,TaskOutput 会跟踪已写入的字节数。当文件超过阈值时,ShellCommandImpl.#handleExit 将设置 outputFilePath,而不会将全部内容加载到 result.stdout

// src/utils/ShellCommand.ts (第 265-280 行)
async #handleExit(code: number): Promise<void> {
  const stdout = await this.taskOutput.getStdout();
  const result: ExecResult = {
    code,
    stdout,
    stderr: this.taskOutput.getStderr(),
    interrupted: code === SIGKILL,
    // ...
  };

  if (this.taskOutput.stdoutToFile && !this.#backgroundTaskId) {
    if (this.taskOutput.outputFileRedundant) {
      // 小文件:内容已在 stdout 中,删除磁盘文件
      void this.taskOutput.deleteOutputFile();
    } else {
      // 大文件:告诉调用者完整输出的位置
      result.outputFilePath = this.taskOutput.path;
      result.outputFileSize = this.taskOutput.outputFileSize;
      result.outputTaskId = this.taskOutput.taskId;
    }
  }
}

call 方法中,大输出会被进一步处理——通过 linkcopyFile 将临时文件迁移到 tool-results 目录,并限制最大 persisted 大小为 64 MB:

// src/tools/BashTool/BashTool.tsx (第 960-980 行)
const MAX_PERSISTED_SIZE = 64 * 1024 * 1024;
if (result.outputFilePath && result.outputTaskId) {
  const fileStat = await fsStat(result.outputFilePath);
  persistedOutputSize = fileStat.size;
  await ensureToolResultsDir();
  const dest = getToolResultPath(result.outputTaskId, false);
  if (fileStat.size > MAX_PERSISTED_SIZE) {
    await fsTruncate(result.outputFilePath, MAX_PERSISTED_SIZE);
  }
  try {
    await link(result.outputFilePath, dest);   // 优先硬链接(零拷贝)
  } catch {
    await copyFile(result.outputFilePath, dest); // 失败则复制
  }
  persistedOutputPath = dest;
}

当输出被持久化后,mapToolResultToToolResultBlockParam 不会将完整内容塞给模型,而是构建一个 <persisted-output> 标记,附带预览片段:

// src/tools/BashTool/BashTool.tsx (第 750-770 行)
if (persistedOutputPath) {
  const preview = generatePreview(processedStdout, PREVIEW_SIZE_BYTES);
  processedStdout = buildLargeToolResultMessage({
    filepath: persistedOutputPath,
    originalSize: persistedOutputSize ?? 0,
    isJson: false,
    preview: preview.preview,
    hasMore: preview.hasMore
  });
}

模型看到这个标记后,可以决定是否需要通过 FileRead 工具读取完整内容。

4.3 退出码处理

不同 Shell 和不同命令的退出码语义差异很大。grep 没找到匹配项时返回 1,但这并不表示错误。Claude Code 通过 interpretCommandResult 函数对退出码进行语义解释:

// src/tools/BashTool/commandSemantics.ts(示意)
const interpretationResult = interpretCommandResult(
  input.command, result.code, result.stdout || '', ''
);

如果 interpretationResult.isErrortrue,且命令并非因用户中断而终止,则会抛出 ShellError

// src/tools/BashTool/BashTool.tsx (第 995-1000 行)
if (interpretationResult.isError && !isInterrupt) {
  throw new ShellError('', outputWithSbFailures, result.code, result.interrupted);
}

PowerShell 的退出码处理更为复杂,因为它同时存在 cmdlet 的 $? 和原生 exe 的 $LASTEXITCODE。Claude Code 的策略是优先使用 $LASTEXITCODE,仅当其为 $null(表示没有原生 exe 运行过)时才回退到 $?

# powershellProvider.ts 生成的跟踪代码
$_ec = if ($null -ne $LASTEXITCODE) { $LASTEXITCODE } elseif ($?) { 0 } else { 1 }

这解决了 git push 2>&1 这类场景的误判:在 PowerShell 5.1 中,即使 git 返回 0,只要 stderr 有输出,$? 就会被设为 $false,导致错误的失败报告。

五、安全策略

5.1 危险命令检测

BashTool 的安全检测是一个多阶段、多层次的防御体系。在 checkPermissions 阶段,bashToolHasPermission 会依次执行:

  1. 同步安全启发式检查:通过正则快速识别子表达式、变量展开等危险模式
  2. AST 安全解析:调用 parseForSecurity 对命令进行树状结构分析
  3. 权限规则匹配:检查用户是否已预先授权过同类命令
  4. 分类器评估:使用机器学习分类器(bashClassifier)判断命令风险等级
  5. 路径约束检查:验证命令是否触及只读目录限制
// src/tools/BashTool/bashPermissions.ts (第 1-30 行)
import {
  checkSemantics, nodeTypeId, type ParseForSecurityResult,
  parseForSecurityFromAst, type Redirect, type SimpleCommand,
} from '../../utils/bash/ast.js';
import {
  type CommandPrefixResult, extractOutputRedirections,
  getCommandSubcommandPrefix, splitCommand_DEPRECATED,
} from '../../utils/bash/commands.js';
import { classifyBashCommand, isClassifierPermissionsEnabled } from '../../utils/permissions/bashClassifier.js';

为了防止复杂复合命令导致解析器过载,代码还设置了子命令数量上限:

// src/tools/BashTool/bashPermissions.ts (第 95-100 行)
export const MAX_SUBCOMMANDS_FOR_SECURITY_CHECK = 50;

超过 50 个子命令的复合指令将直接降级为 "ask"(询问用户),宁可误报也不漏报。

5.2 权限审批

权限系统基于规则匹配而非简单的命令白名单。用户可以对特定前缀(如 git *)或具体命令授权,后续同类命令将自动通过:

// src/tools/BashTool/BashTool.tsx (第 520-540 行)
async preparePermissionMatcher({ command }): Promise<PatternMatcher> {
  const parsed = await parseForSecurity(command);
  if (parsed.kind !== 'simple') {
    return () => true; // 解析失败则走安全路径:运行 hook
  }
  const subcommands = parsed.commands.map(c => c.argv.join(' '));
  return pattern => {
    const prefix = permissionRuleExtractPrefix(pattern);
    return subcommands.some(cmd => {
      if (prefix !== null) {
        return cmd === prefix || cmd.startsWith(`${prefix} `);
      }
      return matchWildcardPattern(pattern, cmd);
    });
  };
}

这里的巧妙之处在于 preparePermissionMatcher 返回一个闭包,它会在权限 hook 的 if 条件中被调用。如果命令是复合命令(如 ls && git push),只要其中任意子命令匹配规则,hook 就会触发,从而避免 ls && git push 因为 ls 没匹配而绕过 git * 规则的情况。

5.3 超时控制

超时机制分布在两个层面:

进程层面ShellCommandImpl 在构造时设置 setTimeout,超时后根据配置决定是 kill(发送 SIGTERM)还是 background:

// src/utils/ShellCommand.ts (第 235-245 行)
static #handleTimeout(self: ShellCommandImpl): void {
  if (self.#shouldAutoBackground && self.#onTimeoutCallback) {
    self.#onTimeoutCallback(self.background.bind(self));
  } else {
    self.#doKill(SIGTERM);
  }
}

Assistant 模式层面:在 Assistant(Kairos)模式下,主 Agent 需要保持响应。阻塞超过 15 秒的命令会被自动 background:

// src/tools/BashTool/BashTool.tsx (第 1105-1115 行)
if (feature('KAIROS') && getKairosActive() && isMainThread && !isBackgroundTasksDisabled) {
  setTimeout(() => {
    if (shellCommand.status === 'running' && backgroundShellId === undefined) {
      assistantAutoBackgrounded = true;
      startBackgrounding('tengu_bash_command_assistant_auto_backgrounded');
    }
  }, ASSISTANT_BLOCKING_BUDGET_MS).unref();
}

ASSISTANT_BLOCKING_BUDGET_MS 被定义为 15,000 毫秒。这种设计让长时间编译或测试可以在后台运行,而主 Agent 继续处理其他任务。

5.4 沙箱限制

沙箱是 Claude Code 安全架构的最后一道防线。shouldUseSandbox 函数综合多项因素决定是否启用沙箱:

// src/tools/BashTool/shouldUseSandbox.ts (第 15-40 行)
function containsExcludedCommand(command: string): boolean {
  // 检查动态配置中的禁用命令(仅内部测试)
  if (process.env.USER_TYPE === 'ant') {
    const disabledCommands = getFeatureValue_CACHED_MAY_BE_STALE(...);
    // ...
  }
  // 检查用户配置的排除命令
  const settings = getSettings_DEPRECATED();
  const userExcludedCommands = settings.sandbox?.excludedCommands ?? [];
  // 对复合命令逐个子命令检查
  // ...
}

用户可以通过 dangerouslyDisableSandbox: true 显式绕过沙箱,但这是一个有明确警告标志的参数,会触发额外的审计日志。

沙箱的执行通过 SandboxManager.wrapWithSandbox 完成。以 Linux 为例,它会在命令外层包裹 bwrap,限制文件系统访问范围、网络能力和进程创建能力。即使命令本身包含恶意代码,沙箱也能将其影响范围限制在隔离环境中。

值得注意的是,后台任务的输出文件会受到大小看门狗(size watchdog)的监控:

// src/utils/ShellCommand.ts (第 200-220 行)
#startSizeWatchdog(): void {
  this.#sizeWatchdog = setInterval(() => {
    void stat(this.taskOutput.path).then(s => {
      if (s.size > this.#maxOutputBytes && this.#status === 'backgrounded') {
        this.#killedForSize = true;
        this.#clearSizeWatchdog();
        this.#doKill(SIGKILL);
      }
    });
  }, SIZE_WATCHDOG_INTERVAL_MS);
}

这个机制源于一次真实的事故:后台任务中的 stuck append loop 曾将输出文件膨胀到 768 GB,几乎耗尽磁盘空间。看门狗每 5 秒检查一次文件大小,超限即强制 SIGKILL

六、总结

BashTool 与 PowerShellTool 共同构成了 Claude Code 与操作系统交互的桥梁。它们的设计体现了工程上的多重考量:

  • 跨平台一致性:通过 ShellProvider 抽象和 buildTool 工厂,两个工具共享 80% 以上的逻辑,同时保留平台特有的语义处理
  • 实时性与响应性AsyncGenerator 进度流、自动后台化、看门狗机制,确保 Agent 不会因单个命令而僵死
  • 安全纵深:从输入校验、AST 分析、权限规则、沙箱隔离到大小监控,多层防御覆盖不同风险面
  • 上下文效率:输出截断、持久化、预览片段,让模型在有限 token 内获取最大信息量

对于希望构建类似 AI Agent 工具的开发者而言,Claude Code 的 Shell 执行层是一个极具参考价值的实现样本。它在简洁的接口之下,隐藏了复杂的进程管理、平台适配和安全工程,这种"简单对外、复杂对内"的设计哲学,正是高质量基础设施代码的标志。