Bash 拒绝规则为什么拦不住:4 次 shell 变量绕过
你的规则写的是"这条命令不能跑",检查器看到的却是"这段文字不像那条命令"。四次修复、四种 shell 行为,以及三层加固清单。
一句话结论:配置文件里写着 "deny": ["Bash(rm *)"],你读成"Agent 不能跑 rm";工具读成"一段文本不匹配 rm * 就不执行"。这两句话之间隔着整个 shell:变量展开、词分割、命令查找、不同 shell 的方言。2026 年 10 月 3 日到 9 日,Claude Code 的 changelog 修掉了四个"shell 变量绕过权限检查"的案例——两次是 deny/ask 规则漏掉了命令,两次是"自动批准"把本该弹窗的命令放了过去。
披露:本文编译自 Gabriel Anhaia 的技术分析(dev.to,2026-10-18)。作者是 Hermes IDE 的作者,文中含产品段落,请按广告阅读。四个修复条目的原文来自 Claude Code 的 CHANGELOG;文中对 shell 行为的解释依据 bash / zsh 手册,作者明确说明他没有在文中实机执行那些示例,行为以手册为准。
一、四条 changelog,四种绕过形状
先把事实摆清楚。2.1.289 到 2.1.296 之间,官方 CHANGELOG 记了这四条:
| 版本 | 日期 | 修复内容(意译) |
|---|---|---|
| 2.1.289 | 10-03 | deny / ask 规则漏掉了"环境变量前缀 + 展开值"后面的命令,例如 TZ="$HOME" rm -rf build;发生在沙箱自动放行模式下 |
| 2.1.289 | 10-03 | 裸变量赋值出现在命令之前时,沙箱自动放行会让 deny / ask 规则被跳过 |
| 2.1.290 | 10-05 | 变量名在 zsh 与 bash 里含义不同的命令被自动批准,现在改为弹窗 |
| 2.1.296 | 10-09 | 给 BASH_ARGV0 赋值后再使用它的命令被自动批准,现在改为弹窗 |
同一周还有"邻居"修复:2.1.290 还修了"变量经由 declare、typeset、export、readonly 前缀赋值,导致规则漏掉命令或路径",以及"rg 这类只读命令在 shell 仍会把参数展开成通配符时被自动批准"。
这四条的共同点不是某个具体命令,而是检查器必须"预测 shell 会做什么",而 shell 的行为比规则语言复杂得多。
二、为什么文本匹配必须"像 shell 一样思考"
权限文档说得很直接:Bash 规则"匹配 Claude 写出的命令文本,在 Claude Code 拆分复合命令、剥掉包装器之后"。同一页也承认:deny / ask 规则"不是程序周围的安全边界",并给了例子——Bash(rm *) 能挡住 rm -rf build/,但挡不住:
/bin/rm -rf build/
bash -c 'rm -rf build/'
文档还写了变量场景的预期行为:"deny / ask 规则会匹配任何前置赋值,所以 deny 里的 Bash(rm *) 仍然匹配 FOO=bar rm -rf tmp/。" 而 2.1.289 修的两个案例,正是这个承诺在沙箱自动放行(sandbox auto-allow)下没有兑现。
为什么这个模式最危险?因为默认情况下,沙箱内的命令可以写当前工作目录。沙箱对 rm -rf build 没有意见——它就在项目里。此时deny 规则是唯一说"不"的东西。它一旦被绕过,就没有第二道闸。
问题的根源是顺序:
Claude 写出一段文本
↓
Claude Code 读文本、匹配规则 ← 规则在这一层
↓
真正的 shell 展开变量、分词、查命令 ← 行为在这一层
↓
命令执行
规则要成立,检查器就得把第二层的行为全部预测对,而且要针对"实际会执行这条命令的那个 shell"预测。
三、形状一与二:前缀赋值 vs 裸赋值
前两个修复针对两种"看起来像、行为不同"的写法。bash 手册在 SIMPLE COMMAND EXPANSION 一节给了规则:
- 有命令名时,变量赋值"被加入被执行的命令的环境,不影响当前 shell 环境";
- 没有命令名时,赋值"影响当前 shell 环境"。
看例子(注释是按手册推的预期结果,不是本文实机运行):
# 前缀:X 进入 printenv 的环境
bash -c 'X=1 printenv X' # 打印 1
# 裸赋值:X 只是 shell 变量
bash -c 'X=1; printenv X' # 什么都不打印
# 前缀 + 展开值
bash -c 'D="$HOME" printenv D' # 打印 home 目录
# 参数在赋值生效前就展开了
bash -c 'D="$HOME" echo "[$D]"' # 打印 []
最后一行最反直觉:bash 里命令后面的词先展开,赋值文本后生效,所以 echo 收到的 $D 还是空。TZ="$HOME" 这种前缀的值,只有 shell 真正执行时才知道落在哪。
裸赋值危险的原因相反:它会留下来。X=1; rm -rf build 是两条语句,第一条没有命令名,于是改变了整行的 shell 环境。changelog 没给第二个修复的具体例子,所以只能把它当成"符合这段描述的某一种形状",不能当成 Anthropic 修的那个确切案例。文档里同类风险写得很清楚:设置、取消或循环某些特殊变量(例如 PATH、IFS)的命令会触发弹窗。
四、形状三:同一行,两个 shell
Claude Code 不是在抽象 shell 里跑命令。会话启动时它会根据你的 shell "source ~/.zshrc、~/.bashrc 或 ~/.profile";在 macOS 上通常是 zsh——Apple 从 10.15 起把它设为新账号默认。
而 zsh 有一些变量名对 bash 毫无特殊含义。最典型的是 path:zsh 手册说它是"一个(冒号分隔的)命令搜索目录数组",与 PATH 绑定——"小写形式是数组,大写形式是标量"。
# bash:path 只是个小写数组,PATH 不受影响
bash -c 'path=(/nowhere); command -v ls || echo no-ls'
# zsh:path 就是 PATH,ls 找不到了
zsh -c 'path=(/nowhere); command -v ls || echo no-ls'
按手册,bash 仍能找到一个 ls(可能来自 /bin),zsh 会打印 no-ls。想象一个"按 bash 推理"的检查器:它看到一个小写数组赋值加一条只读命令,找不到需要弹窗的理由。但在 zsh 下,同一段文本已经改变了 ls 到底指哪个程序。
文档本来就要求"写 PATH 要弹窗";2.1.290 修的是这类"名字在 zsh 里含义不同"的情况。changelog 没列具体覆盖了哪些名字,path 只是 zsh 手册里写明的那个。
五、形状四:BASH_ARGV0 让 $0 变成命令
Bash 5.0(2019-01-07 公布)加了"BASH_ARGV0:展开为 $0,赋值时设置 $0"这条行为。手册的表述一致:"给 BASH_ARGV0 赋值会把 $0 设成同一个值。"
于是一行命令可以在命令位置完全不写程序名:
# 需要 bash 5.0+,用 bash --version 确认
bash -c 'BASH_ARGV0=echo; "$0" ran as echo'
按手册,$0 变成 echo,第二条语句执行 echo ran as echo。如果 bash --version 低于 5.0,BASH_ARGV0 只是普通变量,$0 保持 shell 自己的名字,这一行会失败而不是回显。
检查器要拦住它,必须知道:这个变量会写另一个变量,只在 bash 里,而且只从 5.0 开始。2.1.296 的条目说,现在对"某些这种形状"的命令会要求批准——对一个"文本里没有程序名"的命令,这是正确的默认值。
六、加固:把规则当过滤器,把沙箱当墙
先做最基础的一步:看版本。
claude --version
上面所有修复都在 2.1.296 或更新版本里。低于这个版本,先升级,再谈配置。
然后改变对"规则"的期待。Bash deny 规则是对"模型通常写出的文本"的过滤器,这四次修复说明:要让文本过滤器变成一堵墙,每补一个洞就得教会它一条 shell 规则,而 shell 的规则远不止四条。需要"一定成立"的限制,得加一层不读文本的东西。
沙箱是由操作系统强制执行的(macOS 的 Seatbelt、Linux 与 WSL2 的 bubblewrap),限制的是命令能碰到哪些文件和主机,不管命令怎么拼。但它只覆盖 shell 命令:Claude 的文件工具、MCP server、hooks 都在沙箱之外,走普通权限规则。
一份可用的加固配置:
{
"permissions": {
"deny": ["Bash(rm *)", "Bash(git push *)"]
},
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": false,
"allowUnsandboxedCommands": false,
"filesystem": {
"denyRead": ["~/.ssh", "~/.aws"]
}
}
}
四个开关分别做什么:
| 配置 | 作用 | 代价 |
|---|---|---|
autoAllowBashIfSandboxed: false |
关掉"在沙箱里就自动放行",让 deny/ask 漏掉的形状仍然走弹窗(默认是 true) |
弹窗变多 |
allowUnsandboxedCommands: false |
忽略 dangerouslyDisableSandbox,命令在沙箱里失败也不能"关掉沙箱重试" |
某些工具会在沙箱里跑不起来 |
filesystem.denyRead |
默认沙箱命令能读"机器的大部分",包括 ~/.ssh、~/.aws/credentials;这条对命令启动的每个进程生效 |
依赖这些凭据的构建会失败 |
| 保留 deny 规则 | 仍然能挡住模型日常写出的普通 rm -rf build |
无 |
注意 autoAllowBashIfSandboxed: false 只覆盖了 2.1.289 那两类的形状,它不改变"判断命令安全"的那条独立检查——2.1.290 与 2.1.296 的修复就住在那条检查里。对它们,只有升级有用。
七、自测:把你自己的规则打一遍
升级完之后,用下面这些无害的命令检查"该弹窗的有没有弹窗"。不要用真的 rm,把它们换成 echo:
# 1) 前缀赋值:规则有没有"看到"后面的命令
FOO=bar echo should-be-checked
# 2) 展开值的前缀
TZ="$HOME" echo should-be-checked
# 3) 裸赋值 + 命令
X=1; echo should-be-checked
# 4) zsh 方言(在 zsh 会话里跑)
path=(/nowhere); command -v ls || echo no-ls
# 5) BASH_ARGV0(bash 5.0+)
bash -c 'BASH_ARGV0=echo; "$0" ran as echo'
判据是过程而不是结果:这些命令本身不危险,关键是它们有没有经过你期望的那道检查/弹窗。如果某一条被静默自动批准,说明你的版本或配置还留着缺口。
八、一张图收尾:三层防线,各管一段
| 层 | 管什么 | 不管什么 | 失效时的表现 |
|---|---|---|---|
| 文本规则(deny/ask) | 模型通常怎么写命令 | 任意 shell 方言、路径别名、包装器 | 换个写法就绕过(本文四例) |
| OS 沙箱 | 命令能碰到的文件与网络 | 文件工具、MCP、hooks | 越界被拒,但合法范围内仍可破坏 |
| 审计日志 / 逐轮 diff | 事后知道实际发生了什么 | 事前阻止 | 只能追责与恢复 |
这三层不是替代关系,而是互不重叠的覆盖:规则便宜、沙箱硬、日志可追。作者的一句话值得抄下来:deny 规则在命令运行前做决定,逐轮 diff 告诉你命令实际改了什么,不管它的文本怎么写的。
最后是边界:本文讨论的是权限检查的形状,不是 Claude Code 的内部实现;changelog 没有说明是谁发现的、是否有人真的踩到、检查器内部错在哪。作者明确不猜内部,只按 shell 手册推演——这个态度本身就值得学。任何"我的 deny 规则很安全"的结论,都应该用你自己的命令集去验证一遍。
Sources
- Gabriel Anhaia, 4 Ways a Shell Variable Slipped Past Claude Code's Permission Checks, dev.to, 2026-10-18:https://dev.to/gabrielanhaia/4-ways-a-shell-variable-slipped-past-claude-codes-permission-checks-24b9
- Claude Code CHANGELOG(v2.1.289 / 2.1.290 / 2.1.296):https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md
- 权限与沙箱文档:https://code.claude.com/docs/en/permissions、https://code.claude.com/docs/en/sandboxing
- bash 手册(SIMPLE COMMAND EXPANSION、BASH_ARGV0):https://man7.org/linux/man-pages/man1/bash.1.html
- zsh 手册(
path参数):https://zsh.sourceforge.io/Doc/Release/Parameters.html
相关阅读:Agent / MCP / 自动化完全指南 | Agent 安全:九起事故与六项控制 | MCP 供应链体检:15,341 个服务器扫描
本文由汇智网编译整理,转载请注明出处。文中配置与命令示例需按你的实际版本与环境验证,不构成安全保证。
这是「Agent / MCP」专题的一部分。完整地图见:Agent / MCP / 自动化完全指南