从0打造编码Agent 阅读 + 实验 ≈ 30 分钟 页内实验 ×1
A4 沙箱与隔离:规则拦不住一个自己写文件的 Python 脚本,谁拦? 第 1 课

规则说不许写 ~/.ssh,一个 python -c 脚本写了,谁拦?

谁拦得住
A3 的规则和审批看的都是命令文字。把 deny 规则扩到 bash 文件命令,也拦不住 python -c——它在子进程里才决定写哪个文件。真实录制里,规则按前缀放行了一条复合命令,里头的 python -c 把内容写进了伪 HOME;同一条命令在 Codex 沙箱下被内核拒。改进是换一层:约束进程实际发出的系统调用,包括全部子进程。
❓

本课一问:上一章的规则和审批都放行了 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 这些命令。

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案把 deny 规则扩到 bash 能识别的文件命令

这在「命令名就写在字面上」时有效: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 脚本」。

两道闸:先预测哪道拦下,再看真实录制

下面把 A3 的绕过样例逐条送进两道闸:闸 1 是项目 rules-standard.json 的真实裁决(A3 那一章的配置,last-match),闸 2 是 Codex workspace-write 沙箱。每条命令先预测哪道闸拦下,再揭示本机 sandbox-exec 跑出的真实结果。

两道闸:规则拦命令文字,沙箱拦系统调用 先预测哪道闸拦下,再看 T9 录制的真实结果

闸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}

🧱

{r07codex.stderr}

规则放行的是命令文字,沙箱拦下的是 Python 子进程真实发出的写系统调用。这条 stderr 里的 Operation not permitted 就是内核拒绝的原话——不是 harness 打印的,是操作系统给的。

演进地图

隔离设计的演进:规则拦不住的,交给一层比一层硬的边界
  1. 上一章 只有进程内规则
    A3 权限规则、Pi 的确认弹窗
    解决了在执行前按「工具名 + 命令文字」裁决,拦住模型明着提出的危险调用。
    卡在规则看不见子进程实际做什么:不套沙箱时,python -m unittest …; python -c "写 ~/.ssh" 里权限层按前缀放行(rule_index 8),复合命令里的 python -c 把内容真的写进了伪 HOME(T9 r07)。
  2. 第一代 朴素 OS 沙箱:只放行工作区 你在这里
    allow default 再挖掉写与网络
    解决了约束进程实际发出的系统调用(含全部子进程):写工作区外、联网被内核拒。14 条命令里拦下 6 条。
    卡在工作区里面不设防:同一条写 .git/config 的命令,朴素策略下照样成功(T9 r03)——能借此改 remote、植入 git hook。
  3. Codex Seatbelt(seatbelt_base_policy.sbpl,122 行)
    解决了从 (deny default) 起步,可写根用参数注入,.git / .codex / .agents 用锚定正则单独保护,默认断网。workspace-write 下拦到 9 / 14 条。
    卡在沙箱挡写和联网,不挡读:读伪 HOME 里的假私钥,在四种策略下全部成功(T9 r12)——读到的东西还能经合法命令传出去。
  4. Claude Code(OS 沙箱 + 审批)、claude.ai(gVisor 容器)、Cowork(完整 VM)
    解决了把边界移出进程,按「爆炸半径」分档;规则管「允许调用什么」,隔离管「调用之后能碰到什么」。
    卡在私有数据 + 不可信内容 + 外发能力三者同在时,数据可经已放行的域名外传——规则与沙箱都放行(lethal trifecta,→ 岔路 B1)。
  5. 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 的系统调用越界就被内核拒,与命令怎么写无关
③ 实践上验证了吗?本机录制 T9 r07:同一条命令,不套沙箱时写进了伪 HOME(规则放行),Codex 策略下被拒并给出 Operation not permitted

一句话

📌
规则管的是模型提出的调用,沙箱管的是进程实际做的事;python -c 的写入发生在子进程里,只有沙箱看得见。
🚦

先停在这里:记住「规则看文字、沙箱看系统调用」和「沙箱覆盖全部子进程」两件事,就可以进下一课。
再深一层:展开下面的 Dig,看 Claude Code 沙箱「启用后 deny 路径并入沙箱边界」是什么意思。

🔍 启用沙箱后,deny 规则去哪了?

Claude Code 的文档说:开启沙箱后,权限里的 deny 路径会并入沙箱的边界配置。也就是说,deny 规则不再是「执行前拦一道」的独立检查,而变成了「告诉沙箱哪些路径不可写/不可读」的输入——真正在运行时执行它的是操作系统。规则从「执行机制」降级成了「边界配置」,沙箱才是那个拦得住子进程的执行者。这也解释了为什么两层不是冗余:规则贡献「拦什么」,沙箱贡献「拦得住」。出处:code.claude.com/docs/en/sandboxing(2026-09-26 核对)。

下一个缺口

沙箱到底拦在哪、拦了什么? → 第 2 课:沙箱到底拦在哪、拦了什么?

我们已经看到沙箱能拦下 python -c 写伪 HOME。但它到底放行什么、拒绝什么?为什么工作区里的 .git/config 也要单独保护?下一课把 Codex 的真策略摊开,对 14 条命令逐条看边界。

你可能会问