Yoda
参考Agent 设计指南控制与安全

沙箱 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-runtimeutils/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 挂不了新 /procenableWeakerNestedSandbox 退化为 bind-mount 现有 /proc——文档明确这"显著削弱安全性"(:342, :360)
  • 环境变量不在沙箱边界内:子进程默认继承父进程全部 env(含凭据),需另设 CLAUDE_CODE_SUBPROCESS_ENV_SCRUB(:375)
  • subagent 与父会话同进程、同沙箱配置(:376)

Codex CLI

SandboxPolicy 四态。默认 read-onlyprotocol/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_rootsWritableRoot 结构带 read_only_subpathsprotected_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_policyseatbelt_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 CodeCodex 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
写默认cwdread-only 不可写;workspace-write 限 cwd+/tmp+TMPDIR,.git/.codex 元数据仍只读
网络默认经外置代理,按域名首问审批直接断网(seccomp / 不加网络 sbpl 段),可选 proxy+域名表
逃生门模型参数 dangerouslyDisableSandboxallowUnsandboxedCommands: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.mddocs/sandbox-environments.md
  • CC 源码(重建快照 2026-03-31):utils/sandbox/sandbox-adapter.tstools/BashTool/shouldUseSandbox.tstools/BashTool/prompt.ts
  • Codex 源码(b89ce9a):sandboxing/src/seatbelt.rs + *.sbplsandboxing/src/bwrap.rslinux-sandbox/src/landlock.rsprotocol/src/protocol.rs(SandboxPolicy/WritableRoot)、core/src/tools/sandboxing.rs
  • sandbox-runtime 开源仓(外链):https://github.com/anthropic-experimental/sandbox-runtime

On this page