沙箱 Sandboxing
两家在 macOS 都用 Seatbelt、Linux 都用 bubblewrap 系方案;CC 沙箱只包 Bash 工具、靠外置代理做域名白名单,Codex 沙箱包全部 exec、默认 read-only 且审批即"越狱"通道
沙箱 Sandboxing
结论
底层原语两家高度趋同:macOS 用 Seatbelt(sandbox-exec/sbpl profile),Linux 用 bubblewrap + seccomp(Codex 另保留 Landlock 作 legacy 后端)[一手源码]。差异在架构定位:CC 的沙箱是可选增强——只隔离 Bash 工具及其子进程,默认关闭,开启后用 autoAllowBashIfSandboxed 换取免审批,命令失败时模型可用 dangerouslyDisableSandbox 参数走回常规审批流;Codex 的沙箱是默认地基——sandbox_mode 默认 read-only,所有 shell 执行都在策略内,"绕过沙箱"本身就是审批的标的(见 permissions 章)。网络侧 CC 用外置代理 + 域名白名单(首次访问新域名弹审批),Codex 默认直接断网(seccomp 拦 socket / Seatbelt 不加网络段),可选 proxy 放行。harness 不应自己再造沙箱,而应把两家的沙箱配置面(写路径、域名、逃生门开关)变成可见、可审计的 UI。
研究问题
- 两家在 macOS / Linux 各用什么 OS 原语?policy 怎么生成?
- 文件系统与网络的默认边界分别是什么?
- 沙箱失败后的升级(escalation)路径如何设计?逃生门能不能被管理员焊死?
各 Agent 设计与实现
Claude Code
实现载体。CC 不在主仓里直接写 sandbox profile,而是封装外部包 @anthropic-ai/sandbox-runtime:utils/sandbox/sandbox-adapter.ts:2-17 是适配层,SandboxManager 暴露 isSandboxingEnabled / wrapWithSandbox / initialize 等接口(sandbox-adapter.ts:880-927)[一手源码]。OS 原语:macOS Seatbelt、Linux/WSL2 bubblewrap + socat(网络中继),seccomp filter 可选用于封 Unix domain socket(docs/sandboxing.md:225-231, :85)[一手文档]。WSL1、原生 Windows 不支持(:24, :365)。
默认边界。文件系统:写仅限 cwd 及子目录;读默认全盘开放(含 ~/.ssh、~/.aws/credentials,需手动 denyRead)(docs/sandboxing.md:200-202)[一手文档]。git worktree 场景会额外放行主仓共享 .git(hooks/、config 仍拒写)(:203)。网络:零预批域名,首次访问新域名弹审批,可用 allowedDomains 预批(:212);代理在沙箱外运行、按客户端声称的 hostname 放行、不做 TLS 解密——文档明确提示 domain fronting 数据渗出风险(:352-356)[一手文档]。沙箱还自动拒写各 scope 的 settings.json 与 managed 目录,防止命令改写自己的策略(:361)。
审批联动与逃生门。shouldUseSandbox 决定一条 Bash 命令进不进沙箱:sandbox 未启用→否;dangerouslyDisableSandbox=true 且策略允许→否;命中 excludedCommands→否(tools/BashTool/shouldUseSandbox.ts:129-153)[一手源码]。模型侧的 prompt 写明"默认在沙箱内跑,失败且因沙箱所致时立即带 dangerouslyDisableSandbox: true 重试(别问,直接做)",而当策略禁用逃生门时 prompt 换成"该参数已被策略禁用"(tools/BashTool/prompt.ts:231-253)[一手源码]。三个策略开关的源码默认值:sandbox.enabled 默认 false、autoAllowBashIfSandboxed 默认 true、allowUnsandboxedCommands 默认 true(sandbox-adapter.ts:459-477);failIfUnavailable 默认 false——依赖缺失时降级为无沙箱执行 + 警告(:479-485, docs/sandboxing.md:59)[一手源码]。
免审批语义。沙箱开启 + auto-allow 模式下,沙箱内 Bash 自动放行(连 ask: Bash(*) 都跳过),但显式 deny 规则、rm -rf //~ 熔断仍然生效(docs/permissions.md:317; permissions.ts:1094-1110 中 canSandboxAutoAllow 分支)[一手源码+一手文档]。
排除与匹配细节。excludedCommands 的匹配复用权限规则三态(prefix/exact/wildcard):containsExcludedCommand 把命令拆成候选子串后逐条对 bashPermissionRule 匹配(shouldUseSandbox.ts:100-126)[一手源码]。即排除表的写法与权限规则一致(docker * 等)。其他值得记录的边界 [一手文档]:
- 自定义代理:
sandbox.network.httpProxyPort / socksProxyPort指向企业 MITM 代理,配合enableWeakerNetworkIsolation解决 Go 系 CLI 的 TLS 失败(docs/sandboxing.md:321-331, :340) - 容器内嵌套:非特权容器中 bwrap 挂不了新
/proc,enableWeakerNestedSandbox退化为 bind-mount 现有 /proc——文档明确这"显著削弱安全性"(:342, :360) - 环境变量不在沙箱边界内:子进程默认继承父进程全部 env(含凭据),需另设
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB(:375) - subagent 与父会话同进程、同沙箱配置(:376)
Codex CLI
SandboxPolicy 四态。默认 read-only(protocol/src/config_types.rs:86-96 #[default] ReadOnly),完整策略类型(protocol/src/protocol.rs:854-902)[一手源码]:
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum SandboxPolicy {
DangerFullAccess, // 无任何限制
ReadOnly { network_access: bool }, // 全盘只读,默认断网
ExternalSandbox { network_access: .. }, // 已在外部沙箱内,放开磁盘
WorkspaceWrite {
writable_roots: Vec<AbsolutePathBuf>,
network_access: bool, // 默认 false
exclude_tmpdir_env_var: bool,
exclude_slash_tmp: bool,
},
}workspace-write = 全盘可读 + cwd/TMPDIR//tmp 可写 + 可配 writable_roots。WritableRoot 结构带 read_only_subpaths 与 protected_metadata_names——即使根可写,.codex、.git(尤其 .git/hooks)等提权路径仍强制只读(protocol.rs:906-955 注释明确这是防 agent 自我提权)[一手源码]。ExternalSandbox 变体值得 harness 注意:它是为"宿主已经提供了容器/VM 隔离"的场景准备的官方表达。
macOS:Seatbelt。执行器固定调用 /usr/bin/sandbox-exec(写死绝对路径防 PATH 劫持,sandboxing/src/seatbelt.rs:27-29)[一手源码]。基础 policy 参考 Chrome 沙箱、(deny default) 白名单式放行(sandboxing/src/seatbelt_base_policy.sbpl:1-12);网络开启时追加 seatbelt_network_policy.sbpl(只放行 TLS 证书查询、DNS 配置等 mach service,代理端口规则由 codex-core 按环境注入,sbpl 文件头注释)[一手源码]。写路径规则由 build_seatbelt_access_policy 动态拼接(seatbelt.rs:335+),保护性元数据路径用正则拦截(:392+)。
Linux:bubblewrap + seccomp。主后端为 bwrap(--unshare-user --unshare-net --ro-bind ...,sandboxing/src/bwrap.rs:77-79),WSL1 显式不支持(bwrap.rs:139-158);进程内原语为 no_new_privs + seccomp 网络过滤,Landlock 降级为 legacy 文件系统后端(linux-sandbox/src/landlock.rs:1-4, :42-74)[一手源码]。断网的实现是给当前线程装 network seccomp filter(landlock.rs:67-68),而非代理白名单——这是与 CC 的关键差异:Codex 默认断网是 syscall 级,CC 默认是"代理 + 域名审批"。
网络细粒度策略。requirements/permissions 层支持 NetworkDomainPermissionsToml(allow/deny 域名表)、NetworkUnixSocketPermissionsToml、loopback proxy 端口等(config/src/config_requirements.rs:239-335),Seatbelt 侧有 unix_socket_policy 注入对应 socket 路径参数(seatbelt.rs:171-243)[一手源码]。说明 Codex 也在向"代理路由 + 域名白名单"演进(proxy_routed_network 参数,landlock.rs:47),但默认仍是全断。
Seatbelt policy 的组装顺序。create_seatbelt_command_args 把 policy 按段拼接:base policy(deny default + sysctl/IOKit/mach 白名单)→ restricted read-only 平台默认段(仅 deny-read 策略时附加 restricted_read_only_platform_defaults.sbpl,seatbelt.rs:712-719)→ 写路径段(build_seatbelt_access_policy)→ 网络段 + 动态代理规则(dynamic_network_policy,按 env 中 loopback 代理端口注入,seatbelt.rs:43-92, :245-292)[一手源码]。deny-read glob 通过把 gitignore 风格 pattern 编译为 Seatbelt 正则实现(build_seatbelt_unreadable_glob_policy、seatbelt_regex_for_unreadable_glob,seatbelt.rs:424-510)。
升级路径。命令需要更多权限时不是"沙箱失败→裸跑",而是走审批:SandboxOverride::BypassSandboxFirstAttempt 仅在 execpolicy Allow 显式授信或命令声明 require_escalated 且获批后触发;含 denied-read 的策略下永远不许脱沙箱(core/src/tools/sandboxing.rs:246-291)[一手源码]。Windows 另有独立的 windows-sandbox-rs crate 与 WindowsSandboxLevel(disabled 时信任项目也只给 read-only profile,core/src/config/permissions.rs:48-60)[一手源码]。
差异矩阵
| 维度 | Claude Code | Codex CLI |
|---|---|---|
| 默认状态 | 关闭(sandbox.enabled 默认 false) | 开启(sandbox_mode 默认 read-only) |
| 覆盖范围 | 仅 Bash 工具及子进程;Read/Edit 走权限系统 | 所有 exec/unified_exec/apply_patch 执行 |
| macOS 原语 | Seatbelt(经 @anthropic-ai/sandbox-runtime) | Seatbelt,固定 /usr/bin/sandbox-exec,自带 sbpl(deny-default) |
| Linux 原语 | bubblewrap + socat + 可选 seccomp(封 Unix socket) | bubblewrap + no_new_privs + seccomp 断网;Landlock 为 legacy |
| 读默认 | 全盘可读(credentials 也读得到,需手动 denyRead) | read-only 全盘可读;restricted read-only 可加 deny-read glob |
| 写默认 | cwd | read-only 不可写;workspace-write 限 cwd+/tmp+TMPDIR,.git/.codex 元数据仍只读 |
| 网络默认 | 经外置代理,按域名首问审批 | 直接断网(seccomp / 不加网络 sbpl 段),可选 proxy+域名表 |
| 逃生门 | 模型参数 dangerouslyDisableSandbox(allowUnsandboxedCommands:false 可焊死) | 审批升级(with_escalated_permissions/execpolicy Allow),denied-read 下无条件禁止 |
| 沙箱不可用时 | 默认降级裸跑+警告(failIfUnavailable:true 改为拒启) | 平台不支持时报 SeatbeltUnavailable/WSL1 错误(sandboxing/src/lib.rs:40-45) |
| 自我保护 | 拒写各 scope settings.json 与 managed 目录 | WritableRoot 的 protected_metadata_names(.codex/.git/hooks 等) |
最小复现
# Codex:read-only 下写文件应失败,workspace-write 下写 cwd 应成功
codex debug seatbelt --sandbox read-only -- touch ./should_fail.txt # macOS 专用调试入口
# (cli/src/debug_sandbox/seatbelt.rs 提供该子命令)[一手源码]
# CC:开沙箱后验证 cwd 外写入被 OS 拦截
# .claude/settings.local.json: {"sandbox": {"enabled": true}}
claude -p 'run: touch ~/outside.txt'
# 预期:命令在沙箱内失败;模型可能带 dangerouslyDisableSandbox 重试并触发常规审批
# [一手文档 sandboxing.md:137-139](本机未实测;codex debug seatbelt 子命令存在性来自源码目录 cli/src/debug_sandbox/。)
Harness 接入建议(Yoda 实践)
- 不要在 harness 层重复造沙箱,把两家的原生沙箱当成事实边界:对 Codex 显式传
--sandbox/-c sandbox_mode=...,对 CC 把sandbox.enabled写进 workspace 的.claude/settings.local.json并在 UI 标注。Yoda 目前的autoApproveFlag(--dangerously-bypass-approvals-and-sandbox)等于同时关掉沙箱——产品上应把"自动批准"与"关沙箱"拆成两个开关,Codex 侧自动批准应优先用approval_policy=never+workspace-write而不是 danger-full-access。 - 网络是 harness 最常见的坑:本机走 ClashX 代理的环境下,CC 沙箱子进程不继承系统代理(本仓 CLAUDE.md 已记录),Codex 沙箱默认 seccomp 断网。Yoda 启动 agent 前应探测代理环境并注入
https_proxy到允许的 loopback 代理配置,而不是等命令超时。 - 把逃生门状态可视化:CC 的
dangerouslyDisableSandbox重试与 Codex 的 escalation 审批都意味着"这条命令在沙箱外跑过"——harness 应在 transcript 上给这类命令打显式标记(Yoda 的会话视图按 runtime 事件渲染,可以从 CC 的 tool input 字段 / Codex 的ExecApprovalRequestEvent.reason提取)。 - 企业场景遵守焊死语义:若检测到 CC managed 设置含
allowUnsandboxedCommands:false或 Codex requirements 限制allowed_sandbox_modes,harness 的"关闭沙箱"开关应置灰而非静默失败。
失效条件
- Codex Linux 后端正在从 Landlock 迁移到 bwrap+seccomp(landlock.rs 注释称 Landlock 为 legacy/backup),后续版本可能删除 Landlock 路径
- Codex 网络代理路由(
proxy_routed_network、NetworkDomainPermissions)仍带experimental_network前缀(config_requirements.rs:841),shape 随时会变 - CC 沙箱依赖
@anthropic-ai/sandbox-runtime外部包,包版本升级可能改变 profile 细节而本章引用的 adapter 行号失效 - CC
enabledPlatforms为未文档化设置(sandbox-adapter.ts:495-505 注释),随时可能改名或转正
参考资料
- CC 文档:
claude-code-docs/docs/sandboxing.md、docs/sandbox-environments.md - CC 源码(重建快照 2026-03-31):
utils/sandbox/sandbox-adapter.ts、tools/BashTool/shouldUseSandbox.ts、tools/BashTool/prompt.ts - Codex 源码(b89ce9a):
sandboxing/src/seatbelt.rs+*.sbpl、sandboxing/src/bwrap.rs、linux-sandbox/src/landlock.rs、protocol/src/protocol.rs(SandboxPolicy/WritableRoot)、core/src/tools/sandboxing.rs - sandbox-runtime 开源仓(外链):https://github.com/anthropic-experimental/sandbox-runtime