规则说不许写 ~/.ssh,一个 python -c 脚本写了,谁拦?
本课一问:上一章的规则和审批都放行了 python3 -c "…"。规则说「不许写 ~/.ssh」,可一个 python -c 脚本还是写了——谁拦得住?
这一课先不碰:沙箱内部到底怎么拦、拦了哪些系统调用(第 2 课);各家在沙箱 / 容器 / 不做之间怎么选(第 3 课);数据经合法命令外传(岔路 B1)。
从已有出发
到 A3 为止,harness 在每次调用执行前按「工具名 + 命令文字」裁决:放行、拒绝或问人。这一层拦得住模型明着提出的 rm -rf /,但第 4 课的压轴留了个洞——我们真实运行里模型发出的 bash 只有跑测试这一种(26 次运行、37 次 bash,全是 python -m unittest …),而项目的 rules-standard.json 恰好放行了 python -m unittest 前缀。
问题来了:一条命令可以以放行的前缀开头,却在后面挂一段 python -c。规则读到的是命令文字,python -c 在运行时才决定打开哪个文件——这道缝,规则补不上。
先自己设计:把 deny 规则扩到 bash 里的文件命令
最直接的想法:既然 edit_file 有 deny 规则,那就把「不许写 ~/.ssh」也扩展到 bash——像 Claude Code 那样,让 Read / Edit 的 deny 规则不仅覆盖内置文件工具,也覆盖 cat、head、tail、sed 这些命令。
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
这在「命令名就写在字面上」时有效:cat ~/.ssh/id_rsa 能被识别。但危险不必用这些命令来做:
python3 -c "open(os.path.expanduser('~/.ssh/config'),'a').write(...)"——写文件的是 Python 进程,命令行里根本没有cat/sed。$(echo rm) -rf/python -m x; curl … | sh——命令替换、复合命令,前缀放行了就整条过去。
Claude Code 官方文档自己写明:权限规则不作用于「自己打开文件的任意子进程,比如一个 Python 或 Node 脚本」。
换一层拦:不看命令文字,看进程实际发出的系统调用。不管命令怎么写、派生多少子进程,写 ~/.ssh 这个系统调用越界了,就被内核拒绝。Claude Code 的沙箱作用于 Bash、PowerShell、Monitor 命令及其子进程(原生 Windows 不支持);启用后,deny 路径并入沙箱边界——deny 规则成了边界配置,沙箱才是执行机制。 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条官方文档:沙箱作用于 Bash / PowerShell / Monitor 命令及其子进程;规则不作用于自行打开文件的子进程。 出处Claude Code 文档 · Sandboxing ↗需联网
python -m unittest …; python -c "…写 ~/.ssh/config",权限层按 python -m unittest 前缀放行了整条(规则 #8),不套沙箱时 python -c 把内容真写进了伪 HOME(副作用发生);删除/写入发生在 Python 子进程里,规则根本看不见。 两道闸:先预测哪道拦下,再看真实录制
下面把 A3 的绕过样例逐条送进两道闸:闸 1 是项目 rules-standard.json 的真实裁决(A3 那一章的配置,last-match),闸 2 是 Codex workspace-write 沙箱。每条命令先预测哪道闸拦下,再揭示本机 sandbox-exec 跑出的真实结果。
闸1 = A3 权限层对命令文字的真实裁决(configs/rules-standard.json,last-match);闸2 = 该命令在 codex-workspace-write 策略下的 sandbox_blocked。均为 T9 录制(2026-09-26,伪 HOME);组件不执行命令。
试三件事:① 先看默认选中的 r07(M3 已知漏洞):闸 1 放行(前缀命中规则),真正拦下的是闸 2 沙箱——这就是本课一问的答案;② 点 r06(直接 echo >> ~/.ssh):这次闸 1 就 deny 了,因为规则的 bash 兜底规则拦下了它——但这只是因为它的命令文字没骑上放行前缀;③ 点 r02(跑测试):两道闸都放行,命令正常执行。对比 r07 和 r02——同样以 python -m unittest 开头,闸 1 给了它们一样的放行,差别只在闸 2。
极小真数字:同一条命令,一个没套沙箱、一个套了
r07 的命令是:{r07cmd}
- 不套沙箱(T9 none):退出码 0,伪 HOME 的
~/.ssh/config被真的写入(副作用发生);权限层裁决 = allow(规则 #8)。 - Codex workspace-write(T9 codex):退出码 1,
~/.ssh/config没有被写;stderr 原文:
{r07codex.stderr}
规则放行的是命令文字,沙箱拦下的是 Python 子进程真实发出的写系统调用。这条 stderr 里的 Operation not permitted 就是内核拒绝的原话——不是 harness 打印的,是操作系统给的。
演进地图
- 上一章 只有进程内规则A3 权限规则、Pi 的确认弹窗解决了在执行前按「工具名 + 命令文字」裁决,拦住模型明着提出的危险调用。卡在规则看不见子进程实际做什么:不套沙箱时,
python -m unittest …; python -c "写 ~/.ssh"里权限层按前缀放行(rule_index 8),复合命令里的 python -c 把内容真的写进了伪 HOME(T9 r07)。 - allow default 再挖掉写与网络解决了约束进程实际发出的系统调用(含全部子进程):写工作区外、联网被内核拒。14 条命令里拦下 6 条。卡在工作区里面不设防:同一条写
.git/config的命令,朴素策略下照样成功(T9 r03)——能借此改 remote、植入 git hook。 - Codex Seatbelt(seatbelt_base_policy.sbpl,122 行)解决了从
(deny default)起步,可写根用参数注入,.git/.codex/.agents用锚定正则单独保护,默认断网。workspace-write 下拦到 9 / 14 条。卡在沙箱挡写和联网,不挡读:读伪 HOME 里的假私钥,在四种策略下全部成功(T9 r12)——读到的东西还能经合法命令传出去。 - 第三代 分层:沙箱 + 审批 + 容器Claude Code(OS 沙箱 + 审批)、claude.ai(gVisor 容器)、Cowork(完整 VM)解决了把边界移出进程,按「爆炸半径」分档;规则管「允许调用什么」,隔离管「调用之后能碰到什么」。卡在私有数据 + 不可信内容 + 外发能力三者同在时,数据可经已放行的域名外传——规则与沙箱都放行(lethal trifecta,→ 岔路 B1)。
- 并行选择 干脆不做进程内沙箱Pi 核心、bypassPermissions解决了不做「半个沙箱」以免被误认为边界;坦白这是 YOLO,把真正的隔离交给容器 / 微虚拟机。卡在什么都不套时,T9 的 14 条越界命令全部发生(none 策略拦下 0 条);所以 Pi 说:要边界就上容器。
数字口径:全部由本机 sandbox-exec 加载 Codex 真策略录制(T9,2026-09-26 · macOS 26.6.2 · 伪 HOME,不碰真实 ~/.ssh);a04-sandbox-fixtures.json,2026-09-26 生成。
不是。两层在不同时刻、按不同东西裁决:规则在执行前、按模型提出的命令文字;沙箱在运行中、按进程实际发出的系统调用。规则能理解「你想调 git push」并按你的意图放行或拒绝,但拦不住改写过的写法;沙箱不管你怎么写,只认真实的系统调用,但它不知道你「本来想干什么」。所以它们互补,不能互相替代。
证据三问
| 问 | 本课的回答 |
|---|---|
| ① 它解决什么问题? | 规则和审批看的是命令文字;python -c、命令替换、复合命令让危险发生在规则看不见的地方(子进程、运行时) |
| ② 为什么沙箱能补上? | 沙箱按进程实际发出的系统调用裁决,覆盖全部子进程;写 ~/.ssh 的系统调用越界就被内核拒,与命令怎么写无关 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条官方文档:沙箱作用于 Bash/PowerShell/Monitor 命令及其子进程;这是设计选择,不是数学证明。 出处Claude Code 文档 ↗需联网 |
| ③ 实践上验证了吗? | 本机录制 T9 r07:同一条命令,不套沙箱时写进了伪 HOME(规则放行),Codex 策略下被拒并给出 Operation not permitted 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条项目实测:sandbox-exec 加载 Codex 真策略,伪 HOME,2026-09-26 录制。 出处待补 |
一句话
python -c 的写入发生在子进程里,只有沙箱看得见。先停在这里:记住「规则看文字、沙箱看系统调用」和「沙箱覆盖全部子进程」两件事,就可以进下一课。
再深一层:展开下面的 Dig,看 Claude Code 沙箱「启用后 deny 路径并入沙箱边界」是什么意思。
🔍 启用沙箱后,deny 规则去哪了?
Claude Code 的文档说:开启沙箱后,权限里的 deny 路径会并入沙箱的边界配置。也就是说,deny 规则不再是「执行前拦一道」的独立检查,而变成了「告诉沙箱哪些路径不可写/不可读」的输入——真正在运行时执行它的是操作系统。规则从「执行机制」降级成了「边界配置」,沙箱才是那个拦得住子进程的执行者。这也解释了为什么两层不是冗余:规则贡献「拦什么」,沙箱贡献「拦得住」。
下一个缺口
沙箱到底拦在哪、拦了什么? → 第 2 课:沙箱到底拦在哪、拦了什么?
我们已经看到沙箱能拦下 python -c 写伪 HOME。但它到底放行什么、拒绝什么?为什么工作区里的 .git/config 也要单独保护?下一课把 Codex 的真策略摊开,对 14 条命令逐条看边界。
你可能会问
- 「那规则还有用吗?」 有。规则在执行前按意图裁决,能把「你要
git push」这种合法但需要确认的操作停下来问你,这是沙箱做不到的——沙箱只知道「写这个文件的系统调用越不越界」,不知道你是不是真想推。两层各管一头。 - 「沙箱是不是就万无一失了?」 不是。第 2 课会看到它挡写和网络、挡不住读;岔路 B1 会看到数据还能经已放行的域名外传。沙箱把边界收窄了,但官方自己也说它「不是完整隔离边界」。
- 「Windows 呢?」 Claude Code 的沙箱原生 Windows 不支持(WSL2 里用 bubblewrap);Codex 在 Windows 上用受限令牌,MXC 沙箱在推进中。跨平台是沙箱最难统一的地方,第 3 课会展开。