第六章:BashTool 为什么这么厚
拆 BashTool 怎么把命令协议、安全分析、权限、sandbox 和宿主联动揉成一个工具。
1. 为什么要单独拆它
在 Claude Code 里,BashTool 不是“顺手包了一层 shell”。
它其实把下面这些东西全揉在了一起:
- shell 命令协议
- 命令安全分析
- 权限规则匹配
- sandbox 决策
- 后台任务管理
- 流式进度上报
- 大输出持久化
- 模型侧结果回写
所以如果只把它理解成:
run(command) -> stdout/stderr
那基本等于没看懂这部分设计。
2. 先说结论
我对 BashTool 的判断是:
它本质上是 Claude Code 里的一个“命令执行子系统”,不是普通工具。
它至少有五层:
prompt.ts约束模型什么时候该用 Bash,什么时候不该用。bashPermissions.ts处理规则、语义分析、compound command、classifier、建议规则。readOnlyValidation.ts+pathValidation.ts+modeValidation.ts+shouldUseSandbox.ts处理只读判定、路径风险、模式特判、sandbox 路由。BashTool.tsx处理执行、进度、后台任务、大输出、图片输出。UI.tsx处理终端渲染。
也就是说,Claude Code 并没有把“Bash 的复杂性”交给 shell 本身,而是自己在外面又包了一层完整 runtime。
3. 总体图
flowchart TD
A["模型想执行命令"] --> B["prompt.ts<br/>先告诉模型 Bash 该怎么用"]
B --> C["BashTool.tsx.validateInput"]
C --> D["BashTool.tsx.checkPermissions"]
D --> E["bashPermissions.ts<br/>主权限编排"]
E --> F["readOnlyValidation.ts"]
E --> G["pathValidation.ts"]
E --> H["modeValidation.ts"]
E --> I["shouldUseSandbox.ts"]
D --> J{"allow / ask / deny"}
J -- "allow" --> K["BashTool.tsx.call"]
K --> L["runShellCommand()<br/>AsyncGenerator"]
L --> M["前台执行 / 进度上报"]
L --> N["显式后台化 / 自动后台化"]
M --> O["大输出持久化 / 图片压缩 / cwd 修正"]
N --> O
O --> P["mapToolResultToToolResultBlockParam"]
P --> Q["query.ts 下一轮"]
4. 它先用 prompt 把模型“管住”
源码锚点:
src/tools/BashTool/prompt.ts:172src/tools/BashTool/prompt.ts:231src/tools/BashTool/prompt.ts:245src/tools/BashTool/prompt.ts:260src/tools/BashTool/prompt.ts:275src/tools/BashTool/prompt.ts:287src/tools/BashTool/prompt.ts:317
很多人看工具时只盯 call(),但 Claude Code 这里有一个很明显的设计习惯:
先用 prompt 把模型行为约束住,再用 runtime 做兜底。
BashTool 的 prompt 里至少做了三件事。
4.1 把 Bash 和别的工具切开
它会明确告诉模型:
- 读文件优先用
Read - 改文件优先用
Edit - 写文件优先用
Write - 不要拿
cat/sed/awk/echo去替代这些工具
这很重要,因为作者不想让 Bash 变成“万能降级通道”。
4.2 先在提示词里约束 sandbox 心智
getSimpleSandboxSection() 里写得很明确:
- 默认必须优先跑 sandbox
- 只有看到明确的 sandbox 失败证据,或者用户明确要求,才考虑
dangerouslyDisableSandbox: true - 即使刚刚绕过过 sandbox,下一条命令也要重新默认回 sandbox
- 临时文件要用
$TMPDIR,不要直接写/tmp
这不是安全边界本身,但它能明显降低模型走偏的概率。
4.3 提前教模型怎么做后台任务
prompt 里还会讲:
- 长任务要用
run_in_background - 不要用
sleep轮询 - 已经后台化的任务会通知,不要自己再 poll
这说明作者不是把后台任务当“实现细节”,而是把它当成 BashTool 协议的一部分。
5. BashTool.tsx 暴露出来的,不是一个轻接口
源码锚点:
src/tools/BashTool/BashTool.tsx:420src/tools/BashTool/BashTool.tsx:434src/tools/BashTool/BashTool.tsx:437src/tools/BashTool/BashTool.tsx:445src/tools/BashTool/BashTool.tsx:524src/tools/BashTool/BashTool.tsx:539src/tools/BashTool/BashTool.tsx:555src/tools/BashTool/BashTool.tsx:624
最能说明问题的是这几个方法:
isConcurrencySafe(input)isReadOnly(input)preparePermissionMatcher(input)validateInput(input)checkPermissions(input, context)call(input, context, ..., onProgress)mapToolResultToToolResultBlockParam(output, toolUseId)
这套接口本身就在说明:
BashTool 不是“收到命令,执行一下”,而是“先分析,再决定能不能并发,再决定能不能跑,再决定怎么把结果喂回模型”。
6. 并发安全判定,其实是“只读判定”的副产物
源码锚点:
src/tools/BashTool/BashTool.tsx:434src/tools/BashTool/BashTool.tsx:437src/tools/BashTool/readOnlyValidation.ts:1876
BashTool 的并发逻辑很有代表性:
isConcurrencySafe(input)不是写死的- 它直接依赖
isReadOnly(input) isReadOnly(input)又依赖checkReadOnlyConstraints(...)
也就是说:
在 Claude Code 里,Bash 能不能并发,不是看它是不是 Bash,而是看这条命令在当前分析下能不能被证明为只读。
这背后其实有一套很明确的工程思路:
- 并发不是功能问题,是风险问题
- 不能证明只读,就不要并发
7. “只读”不是靠命令白名单硬猜,而是多层判定
源码锚点:
src/tools/BashTool/readOnlyValidation.ts:1876
checkReadOnlyConstraints() 的流程大致是:
- 先用 shell parser 看命令能不能被可靠解析
- 先看原始命令有没有明显危险模式
- 先挡掉 Windows UNC 路径
- 检查 compound command 里是否同时出现
cd和git - 检查当前目录是不是可疑 bare git repo
- 检查命令是否会写 git internal paths
- 在 sandbox 开启时,检查 git 是否已经偏离 original cwd
- 最后才逐个 subcommand 判断是不是只读
也就是说,它不是:
- “
ls是只读” - “
cat是只读”
这么一层粗判断。
而是:
先排除一批可能把“看起来只读”伪装成危险执行的场景,然后才允许把它当只读。
这点特别像 Claude Code 的整体设计风格:
先做安全收缩,再做能力放行。
8. 真正复杂的地方在 bashPermissions.ts
源码锚点:
src/tools/BashTool/bashPermissions.ts:991src/tools/BashTool/bashPermissions.ts:1050src/tools/BashTool/bashPermissions.ts:1183src/tools/BashTool/bashPermissions.ts:1270src/tools/BashTool/bashPermissions.ts:1459src/tools/BashTool/bashPermissions.ts:1760
8.1 这不是一个简单规则匹配器
bashPermissions.ts 里同时在做:
- exact match / prefix / wildcard 规则匹配
- safe wrapper 和 env var 剥离
- AST / tree-sitter 语义分析
- compound command 拆分
- operator 权限检查
- classifier 接入
- prefix rule 建议生成
- sandbox auto-allow
所以它本质上更像:
一个命令级策略引擎。
8.2 它的主流程可以这么理解
flowchart TD
A["原始命令"] --> B["AST / parseForSecurity"]
B --> C["语义检查"]
C --> D["sandbox auto-allow ?"]
D --> E["exact rule 检查"]
E --> F["bash prompt deny/ask classifier"]
F --> G["operator / pipeline 检查"]
G --> H["原始命令安全检查"]
H --> I["拆 subcommands"]
I --> J["cd + git / 多 cd / fanout 上限"]
J --> K["逐 subcommand 权限检查"]
K --> L["原始命令 redirection/path 校验"]
L --> M["合并结果、生成建议规则、附带 pending classifier"]
这个流程里最重要的不是“有很多步骤”,而是作者在非常努力地保证两件事:
- 不能让 compound command 把规则绕过去
- 不能让 parser 的历史缺陷把安全检查绕过去
9. 它把规则分成了三层
源码锚点:
src/tools/BashTool/bashPermissions.ts:991src/tools/BashTool/bashPermissions.ts:1050src/tools/BashTool/bashPermissions.ts:1183
9.1 bashToolCheckExactMatchPermission
这一层只看完整命令的 exact rule:
- exact deny
- exact ask
- exact allow
- 否则 passthrough,并顺手给一个 exact suggestion
9.2 bashToolCheckPermission
这是真正的“单个 subcommand 权限判定器”,顺序大致是:
- exact rule
- prefix/wildcard deny/ask
- path constraints
- allow rule
- sed constraints
- mode-specific allow
- read-only allow
- 否则 passthrough
这里的设计味道很明显:
Claude Code 先检查显式规则,再检查路径风险,再给模式和只读做自动放行。
9.3 checkCommandAndSuggestRules
这一层是在“单个命令已经大体判完”之后,再补一件事:
- 如果没有显式规则命中,就帮 UI 生成更合理的“别再问了”规则建议
也就是说,作者把:
- 权限判定
- 规则建议
拆成了两个相邻但不完全相同的阶段。
这点做得很细。
10. sandbox auto-allow 不是无脑放行
源码锚点:
src/tools/BashTool/bashPermissions.ts:1270src/tools/BashTool/shouldUseSandbox.ts:130
这是 BashTool 里最容易被误解的一点。
表面上看:
- sandbox 开了
autoAllowBashIfSandboxed开了
好像就直接 allow 了。
但实际上,作者在 checkSandboxAutoAllow() 里先做了这些事:
- 先看 full command 有没有显式 deny / ask
- compound command 还要逐 subcommand 再看 deny / ask
- deny 优先级高于 ask
- 没有显式规则挡路,才 allow
所以这套逻辑真正表达的是:
sandbox auto-allow 只是在“没有更强显式规则反对”的前提下,给 Bash 一条快速通道。
10.1 shouldUseSandbox() 的判断也很克制
shouldUseSandbox() 本身只做几件事:
- 全局 sandbox 没开,直接 false
- 用户显式要求
dangerouslyDisableSandbox且策略允许,false - 命中
excludedCommands,false - 其他情况,true
而且源码注释还明确写了:
excludedCommands 是用户便利功能,不是安全边界。
这说明作者脑子里分得很清楚:
- 哪些是 UX 便利
- 哪些才是真安全控制
11. validateInput() 不是 schema 校验,而是运行前治理
源码锚点:
src/tools/BashTool/BashTool.tsx:322src/tools/BashTool/BashTool.tsx:524
validateInput() 里最典型的一条规则是:
- 如果启用了 Monitor 能力
- 又不是显式后台运行
- 还写了
sleep N
那就直接拦下来。
这说明它不是只做“参数类型是否合法”的校验,而是在做:
这条命令是不是符合 Claude Code 希望的工作方式。
也就是说,validateInput() 已经开始体现产品策略了。
12. 真正执行时,它走的是一个异步生成器
源码锚点:
src/tools/BashTool/BashTool.tsx:624src/tools/BashTool/BashTool.tsx:826src/tools/BashTool/BashTool.tsx:905src/tools/BashTool/BashTool.tsx:974src/tools/BashTool/BashTool.tsx:1008src/tools/BashTool/BashTool.tsx:1110src/tools/BashTool/BashTool.tsx:1113
call() 里面不是直接 await exec(...),而是进入 runShellCommand() 这个 AsyncGenerator。
这件事非常关键,因为它决定了 BashTool 的本质不是“返回一个结果”,而是“持续产生状态变化”:
- 可能先无输出
- 可能开始吐 progress
- 可能前台跑着跑着进后台
- 可能用户手动后台化
- 可能 assistant 模式超时后自动后台化
- 可能最后直接完成
换句话说:
BashTool 的执行模型是流式状态机,不是一次性 RPC。
13. 后台任务设计非常重
源码锚点:
src/tools/BashTool/BashTool.tsx:55src/tools/BashTool/BashTool.tsx:57src/tools/BashTool/BashTool.tsx:905src/tools/BashTool/BashTool.tsx:930src/tools/BashTool/BashTool.tsx:974src/tools/BashTool/BashTool.tsx:982src/tools/BashTool/BashTool.tsx:1113
13.1 三种后台化路径
我看到至少三条路:
- 模型显式传
run_in_background: true - 命令超时后自动后台化
- assistant 模式下,超过
15sblocking budget 自动后台化
这说明作者把“后台任务”看成了一等公民,不是 shell 自己 & 的语法糖。
13.2 前台任务也会先注册成可后台化对象
在超过 PROGRESS_THRESHOLD_MS = 2000 后:
- 它会注册 foreground task
- UI 里显示
BackgroundHint - 用户可以用 Ctrl+B 把它转成后台任务
所以这里真正的抽象不是“前台 / 后台命令”,而更像:
一个 shell task,当前可能以前台或后台方式被宿主观察。
14. 输出处理也不是简单的 stdout/stderr
源码锚点:
src/tools/BashTool/BashTool.tsx:555src/tools/BashTool/BashTool.tsx:591src/tools/BashTool/BashTool.tsx:733src/tools/BashTool/BashTool.tsx:749src/tools/BashTool/BashTool.tsx:798src/tools/BashTool/BashTool.tsx:812
BashTool 对输出做了很多次加工:
- 大输出会持久化到磁盘,再给模型一个可读路径提示
- 图片输出会做识别和压缩
- sandbox 失败信息会被注释进输出
- assistant auto background 的提示和用户手动 background 的提示不一样
- UI 看到的结果和模型看到的
tool_result不是一回事
这说明作者非常明确地区分了两条输出通道:
- 给用户看的
- 给模型继续推理看的
这个分离对 C# 版非常值得保留。
15. 我认为 BashTool 暴露了 Claude Code 的三个核心设计偏好
15.1 偏好一:尽量把“能自动证明安全”的命令自动放掉
例子:
- 只读命令自动 allow
acceptEdits模式下部分文件系统命令自动 allow- sandbox auto-allow 在没有显式规则冲突时直接放
15.2 偏好二:一旦无法稳定证明安全,就保守地 ask
例子:
- AST 太复杂
- subcommand fanout 太多
- process substitution
- UNC path
- compound
cd + git - 可疑 git internal path 写入
15.3 偏好三:长任务别卡住主 agent
例子:
- 显式
run_in_background - timeout 自动后台化
- assistant blocking budget 自动后台化
- 前台任务可随时转后台
这三条其实正好对应 Claude Code 的三种产品目标:
- 尽量少烦用户
- 但别把安全边界冲穿
- 也别把 agent 自己卡死
16. C# 版我建议怎么拆
我不建议做一个“大而全的 BashTool 类”,然后把所有逻辑全塞进去。
我更建议拆成下面几层。
16.1 权限与安全层
public interface IBashPermissionService
{
Task<PermissionResult> CheckAsync(
BashCommandInput input,
RuntimeContext context,
CancellationToken cancellationToken = default);
}
public interface ICommandSafetyAnalyzer
{
Task<CommandSafetyResult> AnalyzeAsync(
string command,
CancellationToken cancellationToken = default);
}
public interface IReadOnlyCommandAnalyzer
{
PermissionResult Check(BashCommandInput input, bool compoundCommandHasCd);
}
public interface IPathConstraintChecker
{
PermissionResult Check(
BashCommandInput input,
string cwd,
PermissionContext context,
bool compoundCommandHasCd);
}
public interface ISandboxPolicy
{
bool ShouldUseSandbox(BashCommandInput input, RuntimeContext context);
}
16.2 执行层
public interface IBashExecutionService
{
IAsyncEnumerable<BashExecutionEvent> ExecuteAsync(
BashCommandInput input,
BashExecutionContext context,
CancellationToken cancellationToken = default);
}
public interface IBackgroundTaskCoordinator
{
Task<string> SpawnAsync(BashTaskSpec spec, CancellationToken cancellationToken = default);
bool TryBackgroundForegroundTask(string taskId);
}
16.3 序列化层
public interface IBashResultSerializer
{
ToolResultBlock SerializeForModel(BashExecutionResult result, string toolUseId);
}
16.4 聚合层
public sealed class BashToolFacade
{
public required IBashPermissionService PermissionService { get; init; }
public required IBashExecutionService ExecutionService { get; init; }
public required IBashResultSerializer ResultSerializer { get; init; }
public required ISandboxPolicy SandboxPolicy { get; init; }
}
17. 如果你要先做原型,我建议先抄这条最小闭环
最小可用版本不要一开始全抄。
先做这条链就够了:
BashCommandInputIBashPermissionService先实现 exact/prefix rule + read-only + path constraintsIBashExecutionService先支持前台执行 + progress +run_in_backgroundIBashResultSerializer先把大输出持久化和普通文本结果分开- 再补
assistant 15s auto background
这样你能先把 BashTool 最关键的设计骨架跑起来,而不是一开始就陷进 parser 细节里。
18. 这一轮拆出来的核心结论
如果只用一句话总结这篇,那就是:
BashTool 是 Claude Code 里最像“操作系统适配层 + agent 策略层混合体”的一个工具。
它真正重要的不是“会执行 bash”,而是这几个设计:
- 先用 prompt 管模型
- 再用权限系统管风险
- 再用后台任务管时延
- 最后把结果拆成“给用户看”和“给模型继续推理”
这四件事,才是你后面做 C# 版时最值得保留的东西。
19. 下一步建议
BashTool 再往下最值得继续拆的,有两个方向:
-
专拆
bashPermissions.ts
这一篇可以把 exact / prefix / compound / classifier / suggestion 彻底画成状态图。 -
专拆 shell 安全分析
也就是readOnlyValidation.ts+pathValidation.ts+bashSecurity.ts,把 Claude Code 到底在防哪些命令级绕过单独整理出来。
如果你的目标是尽快落 C#,我建议先走第 1 个,因为它更贴近运行时抽象。