4.6 BashTool 深度解析
BashTool 是整个工具系统中最复杂的工具——它的实现分布在 src/tools/BashTool/ 目录的 18 个源文件中,涵盖安全验证、权限管理、沙箱隔离、命令语义解析等多个子系统。这种复杂性源于一个根本矛盾:Shell 几乎可以做任何事,是最强大的工具;一条恶意命令就能删除整个文件系统,它又是最危险的工具。
BashTool 的输入 Schema:
{
command: string // Shell 命令
timeout?: number // 超时(毫秒)
description?: string // 活动描述(UI 展示)
run_in_background?: boolean // 异步执行
dangerouslyDisableSandbox?: boolean // 禁用沙箱
}4.6.1 安全验证(bashSecurity.ts)
安全验证是 BashTool 的第一道防线,在权限检查之前执行。bashSecurity.ts 约 2600 行代码,实现了 23 个命名安全检查(通过 BASH_SECURITY_CHECK_IDS 常量映射为数字 ID,避免在遥测日志中记录可变字符串):
const BASH_SECURITY_CHECK_IDS = {
INCOMPLETE_COMMANDS: 1, // 不完整的命令片段
JQ_SYSTEM_FUNCTION: 2, // jq 的 system() 函数调用
JQ_FILE_ARGUMENTS: 3, // jq 文件参数限制
OBFUSCATED_FLAGS: 4, // 混淆的命令标志
SHELL_METACHARACTERS: 5, // Shell 元字符
DANGEROUS_VARIABLES: 6, // 管道/重定向中的危险变量
NEWLINES: 7, // 未引用内容中的换行符
DANGEROUS_PATTERNS_COMMAND_SUBSTITUTION: 8, // 命令替换
DANGEROUS_PATTERNS_INPUT_REDIRECTION: 9, // 输入重定向
DANGEROUS_PATTERNS_OUTPUT_REDIRECTION: 10, // 输出重定向
IFS_INJECTION: 11, // IFS 变量注入
GIT_COMMIT_SUBSTITUTION: 12, // git commit 中的替换
PROC_ENVIRON_ACCESS: 13, // /proc/self/environ 访问
MALFORMED_TOKEN_INJECTION: 14, // 畸形 token 注入
BACKSLASH_ESCAPED_WHITESPACE: 15, // 反斜杠转义空白
BRACE_EXPANSION: 16, // 花括号展开
CONTROL_CHARACTERS: 17, // 控制字符
UNICODE_WHITESPACE: 18, // Unicode 空白同形字
MID_WORD_HASH: 19, // 词中 # 注释攻击
ZSH_DANGEROUS_COMMANDS: 20, // Zsh 危险命令
BACKSLASH_ESCAPED_OPERATORS: 21, // 反斜杠转义运算符
COMMENT_QUOTE_DESYNC: 22, // 注释-引号脱同步攻击
QUOTED_NEWLINE: 23, // 引号内换行符
}命令替换阻断是最关键的防御。COMMAND_SUBSTITUTION_PATTERNS 数组包含 12 种模式,覆盖了所有已知的命令替换形式:
const COMMAND_SUBSTITUTION_PATTERNS = [
{ pattern: /<\(/, message: 'process substitution <()' },
{ pattern: />\(/, message: 'process substitution >()' },
{ pattern: /=\(/, message: 'Zsh process substitution =()' },
// Zsh EQUALS 展开:=curl evil.com → /usr/bin/curl evil.com
// 绕过 Bash(curl:*) deny 规则,因为解析器看到的基命令是 =curl 而非 curl
{ pattern: /(?:^|[\s;&|])=[a-zA-Z_]/, message: 'Zsh equals expansion (=cmd)' },
{ pattern: /\$\(/, message: '$() command substitution' },
{ pattern: /\$\{/, message: '${} parameter substitution' },
{ pattern: /\$\[/, message: '$[] legacy arithmetic expansion' },
{ pattern: /~\[/, message: 'Zsh-style parameter expansion' },
{ pattern: /\(e:/, message: 'Zsh-style glob qualifiers' },
{ pattern: /\(\+/, message: 'Zsh glob qualifier with command execution' },
{ pattern: /\}\s*always\s*\{/, message: 'Zsh always block (try/always construct)' },
{ pattern: /<#/, message: 'PowerShell comment syntax' }, // 纵深防御
]这些模式里有几个要单独说明:
- Zsh equals 展开
=curl:在 Zsh 中,=cmd会展开为cmd的完整路径,等同于$(which cmd)。攻击者可以用=curl evil.com绕过Bash(curl:*)的 deny 规则,因为权限系统解析到的基命令是=curl而不是curl。 - Zsh glob qualifiers
(e:和(+:Zsh 的 glob 限定符可以在文件名匹配过程中执行任意代码——这是一个常被忽略的代码执行向量。 - PowerShell 注释语法
<#:虽然 Claude Code 不在 PowerShell 中执行命令,但作为纵深防御,以防未来引入 PowerShell 执行路径。
为了避免误报(例如在字符串字面量 echo '$(...)' 中错误地检测到命令替换),安全验证器使用 extractQuotedContent() 函数先剥离引号内的内容。这个函数逐字符迭代,跟踪单引号/双引号状态,产出三种变体:withDoubleQuotes 仅剥离单引号内容;fullyUnquoted 剥离所有引号内容;unquotedKeepQuoteChars 剥离内容但保留引号字符本身,用于检测引号邻接。
Zsh 危险命令同样有专门的防御。ZSH_DANGEROUS_COMMANDS 集合包含 18 个命令:
zmodload:Zsh 模块加载器,是多种攻击的入口——zsh/mapfile借数组赋值做不可见的文件 I/O,zsh/zpty提供伪终端命令执行,zsh/net/tcp可用ztcp外泄网络数据,zsh/files的内置rm/mv/ln/chmod绕过二进制检查emulate -c:一个等效于eval的结构,可以执行任意代码sysopen/sysread/syswrite/sysseek:来自zsh/system模块的细粒度文件描述符操作zf_*内置命令,即zf_rm、zf_mv、zf_ln、zf_chmod等:zsh/files模块提供的内置文件操作,绕过了对外部二进制的权限检查
Tree-sitter AST 解析提供了结构化的命令分析能力。当 tree-sitter-bash 可用时,parseCommandRaw() 将命令解析为 AST,parseForSecurityFromAst() 从中提取 SimpleCommand[](已解析引号的简单命令列表)。如果 AST 分析发现命令结构过于复杂(包含命令替换、展开、复杂控制流),它返回 'too-complex',触发 ask 行为——即要求用户确认。当 tree-sitter 不可用时(如某些平台),系统回退到基于正则的遗留解析路径。checkSemantics() 函数在 AST 级别校验语义,识别语法合法但语义危险的命令。
4.6.2 多层权限系统(bashPermissions.ts)
bashToolHasPermission()(src/tools/BashTool/bashPermissions.ts)是整个代码库中最复杂的权限函数,实现了以下分层处理:
第 1 步:AST 解析与复杂度判断
首先尝试使用 tree-sitter 解析命令。解析结果分为三种:
'simple':命令结构简单,可以按子命令逐一检查'too-complex':包含复杂结构(嵌套替换、管道链等),无法保证安全——跳过详细分析,直接进入ask路径'parse-unavailable':tree-sitter 不可用,回退到遗留的splitCommand_DEPRECATED()正则拆分
第 2 步:子命令拆分与上限保护
复合命令(cmd1 && cmd2 || cmd3)被拆分为子命令数组,每个子命令独立检查。为防止 CPU 耗尽(恶意构造的复合命令可能导致正则拆分产生指数级增长的子命令),拆分数量设有硬性上限:
export const MAX_SUBCOMMANDS_FOR_SECURITY_CHECK = 50超过 50 个子命令时,系统放弃逐一分析,直接返回 ask——这是一个安全的回退:无法证明安全的就让用户决定。
第 3 步:安全环境变量剥离
在匹配权限规则之前,系统会先剥离命令前面的安全环境变量赋值:NODE_ENV=prod npm run build 剥掉 NODE_ENV=prod,剩下 npm run build 用于规则匹配。SAFE_ENV_VARS 集合包含 32 个已知安全的变量:Go 系列如 GOEXPERIMENT/GOOS/GOARCH,Rust 系列如 RUST_BACKTRACE/RUST_LOG,Node 的 NODE_ENV,locale 变量如 LANG/LC_ALL 等。内部构建另有 ANT_ONLY_SAFE_ENV_VARS 扩展集,对 USER_TYPE === 'ant' 生效。
为什么要区分安全和不安全的环境变量?因为 MY_VAR=val command 中的 MY_VAR 可能影响命令行为(如 LD_PRELOAD=evil.so curl),不能无条件剥离。但 NODE_ENV=prod 是无害的,如果不剥离,用户设置的 Bash(npm run:*) 规则就无法匹配到 NODE_ENV=prod npm run build。
第 4 步:前缀提取与规则建议
getSimpleCommandPrefix() 从命令中提取稳定的 2 词前缀用于可复用的权限规则。例如:
| 命令 | 提取的前缀 | 建议的规则 |
|---|---|---|
git commit -m "fix typo" | git commit | Bash(git commit:*) |
npm run build | npm run | Bash(npm run:*) |
NODE_ENV=prod npm run build | npm run(剥离安全环境变量后) | Bash(npm run:*) |
ls -la | null(-la 是标志不是子命令) | 仅提供精确匹配规则 |
bash -c "rm -rf /" | null(bash 被阻止生成前缀规则) | 不建议前缀规则 |
注意最后一行:bash、sh、sudo、env 等裸 shell 前缀被显式阻止生成前缀规则,因为 Bash(bash:*) 等同于 Bash(*)——这会意外允许所有命令。
第 5 步:复合命令权限聚合
对于复合命令,所有子命令必须独立通过权限检查。任一子命令被 deny 则整体 deny,任一子命令需要 ask 则整体 ask。建议规则的数量有上限:
export const MAX_SUGGESTED_RULES_FOR_COMPOUND = 5超过 5 条时,权限弹窗退化为"similar commands"描述,而非列出每一条。这避免了用户在一条 && 链中保存 10+ 条规则的混乱体验。
4.6.3 沙箱模式(shouldUseSandbox.ts)
BashTool 支持在沙箱中执行命令,限制文件系统访问、网络和进程能力。沙箱决策逻辑 shouldUseSandbox() 在以下条件下返回 false,即不使用沙箱:
SandboxManager.isSandboxingEnabled()返回false(全局禁用)- 命令设置了
dangerouslyDisableSandbox: true且策略允许绕过 - 命令匹配用户配置的
excludedCommands排除列表
平台支持:macOS 使用 sandbox-exec 配置文件,Linux 使用 bubblewrap(bwrap)提供类 landlock 的限制。排除命令的处理涉及复合命令拆分、环境变量剥离和通配符匹配——与权限系统共享相同的命令解析基础设施。
4.6.4 sed 验证(sedValidation.ts)
sed 命令有专门的验证层,防止它被用作绕过 FileEditTool 权限的后门。验证采用白名单策略——只有已知安全的模式才会自动放行:
安全模式 1:纯行打印,必须有 -n 标志
sed -n '5p'(打印第 5 行)sed -n '1,10p'(打印 1-10 行)sed -n '1p;5p;10p'(打印多个指定行)
安全模式 2:替换表达式
sed 's/foo/bar/g'(替换操作,但 flags 仅允许g/p/i/I/m/M/1-9)
被阻断的危险操作:
w/W标志(文件写入)e/E标志(命令执行——sed可以通过e标志执行 shell 命令!)!(地址取反)\{\}块(sed 脚本块)- 非 ASCII 字符(Unicode 同形字检测)
- 反斜杠分隔符
s\(潜在的解析混淆)
当 sed 命令包含文件参数时,如 sed -i 's/foo/bar/' file.txt,in-place 编辑标志 -i 必须明确存在,且需要文件写入权限。纯读模式下的 sed 不允许操作文件参数。
4.6.5 路径验证与破坏性命令警告
路径验证(pathValidation.ts)从涉及文件路径的命令中提取路径参数,验证它们是否在允许的工作目录范围内;覆盖 cd、rm、mv、cp、cat、grep 等约 36 类命令,即 PATH_EXTRACTORS 的键。不同命令有不同的路径提取规则——cd 将所有参数拼接为一个路径,find 收集第一个非全局标志之前的路径,grep/rg 在解析完 pattern 参数后收集文件路径。所有命令都尊重 POSIX 的 -- 分隔符(之后的所有参数都是位置参数而非标志)。
对于危险的删除路径(rm -rf /、rm -rf ~),无论用户有什么已保存的规则,都始终需要显式批准——这是一个不可覆盖的安全硬限制。
破坏性命令警告(destructiveCommandWarning.ts)是一个纯信息层——它不影响权限决策,只在权限弹窗中显示额外警告。检测的模式包括:
| 类别 | 模式 | 警告消息 |
|---|---|---|
| Git 数据丢失 | git reset --hard | "may discard uncommitted changes" |
| Git 历史覆写 | git push --force / -f | "may overwrite remote history" |
| Git 安全旁路 | --no-verify | "may skip safety hooks" |
| Git 提交覆写 | git commit --amend | "may rewrite the last commit" |
| 递归强制删除 | rm -rf | "may recursively force-remove files" |
| 数据库 | DROP TABLE / TRUNCATE | "may drop or truncate database objects" |
| 数据库 | DELETE FROM table;(无 WHERE) | "may delete all rows" |
| 基础设施 | kubectl delete | "may delete Kubernetes resources" |
| 基础设施 | terraform destroy | "may destroy Terraform infrastructure" |
4.6.6 后台任务管理
BashTool 支持两种后台执行模式:
显式后台化:模型在参数中设置 run_in_background: true,命令从一开始就作为 LocalShellTask 异步执行。输出流式写入任务输出文件,模型可通过 TaskGetTool 轮询结果。
自动后台化:在长时间运行对话的助手模式下,阻塞命令在 15 秒(ASSISTANT_BLOCKING_BUDGET_MS = 15_000)后自动转为后台执行。系统调用 backgroundExistingForegroundTask() 将前台任务移至后台,释放主循环继续处理。这防止了一个长时间运行的 npm install 或 make build 阻塞整个对话。
4.6.7 命令语义(commandSemantics.ts)
BashTool 不只是机械地检查退出码——它理解不同命令的语义约定。标准 Unix 约定中,退出码 0 = 成功,非 0 = 失败,但这并不普适:
| 命令 | 退出码 0 | 退出码 1 | 退出码 2+ |
|---|---|---|---|
grep | 找到匹配 | 无匹配(不是错误) | 真正的错误 |
rg | 找到匹配 | 无匹配(不是错误) | 真正的错误 |
diff | 无差异 | 有差异(不是错误) | 真正的错误 |
test / [ | 条件为真 | 条件为假(不是错误) | 语法错误 |
find | 成功 | 部分成功(某些目录不可访问) | 真正的错误 |
interpretCommandResult() 根据命令名查表解读退出码,避免模型将 grep 的"无匹配"误判为执行失败而发起不必要的重试。
4.6.8 命令分类(UI 展示)
BashTool 还维护了一个命令分类系统,用于 UI 中的折叠显示。isSearchOrReadBashCommand() 函数分析管道中的每个部分,只有当所有部分都是搜索/读取命令时才标记为可折叠:
| 类别 | 命令 | UI 行为 |
|---|---|---|
| 搜索 | find, grep, rg, ag, ack, locate, which, whereis | 折叠为"Searched..." |
| 读取 | cat, head, tail, less, more, wc, stat, file, jq, awk, cut, sort, uniq, tr | 折叠为"Read..." |
| 列表 | ls, tree, du | 折叠为"Listed..." |
| 语义中性 | echo, printf, true, false, : | 跳过(不影响管道分类) |
语义中性命令在管道分类中直接跳过——例如 ls dir && echo "---" && ls dir2 仍然算读取操作,不会因为 echo 而变成不可折叠。