Skip to content

Codex CLI Sandbox 深度调研:设计思想与执行原理

视角:操作系统安全 + Agent 安全模型 + 上下文工程 范围:OpenAI Codex(CLI / TUI / IDE Extension)官方公开实现 + Rust 源码 + 社区分析 代码锚点:本地 trpc-codex/codex-rs/ (早期 0.x 形态) + 上游 openai/codex 0.115+ ~ v0.2.0-alpha.2 时代的稳定形态 时间锚点:2026-04


0. TL;DR:一句话理解 Codex Sandbox

Codex 不沙盒它自己,它沙盒它给 LLM 调用的"那只手"——shell / apply_patch 子进程。

设计核心是 "双轴模型 + 平台原生隔离 + 失败回退"

  • 轴 1:SandboxPolicy——技术上能做什么(OS 强制的文件/网络边界);
  • 轴 2:AskForApproval——什么时候要问你(流程上的人机闸口);
  • 平台原生:macOS 用 Seatbelt / sandbox-exec,Linux 用 Landlock + seccomp(新版叠加 bubblewrap),Windows 用 Restricted Token
  • 失败回退:被沙盒拒绝时不是直接报错,而是 "升级问用户:要不要不带沙盒重跑?"——这条路径是 Codex 与 Claude Code 等竞品最显著的差异。

主进程本身不进沙盒:维护 LLM 会话、读 ~/.codex/、写日志、调用 OpenAI API。只有 LLM 自己生成的那条命令进沙盒。这是一个清醒的取舍——把"模型这个不可信源"严格关在 OS 级别的 jail 里,而不是把整个工具关在容器里。


1. 为什么需要 Sandbox:威胁模型与设计哲学

1.1 Codex 面对的三类威胁

威胁类别具体场景Sandbox 的对策
Prompt InjectionREADME/issue/网页里藏着"删除 ~/.ssh"指令,模型被诱导执行OS 级写保护:~/.ssh 在 writable_roots 之外,syscall 直接被拒
模型幻觉/失误模型本意修补 bug,实际 rm -rf .git.git/ 在 writable_roots 内但被强制 read-only 子路径剔除
第三方 MCP / 脚本污染Skill 脚本或 MCP 工具被篡改后偷偷外发数据默认无网络(network_access=false),即便文件被改也跑不出去

1.2 设计哲学:把不可信源关进 OS 级 jail

OpenAI 在 Codex 文档中明确表态:

By default, the agent runs with network access turned off. Locally, Codex uses an OS-enforced sandbox that limits what it can touch (typically to the current workspace), plus an approval policy that controls when it must stop and ask you before acting.

三条原则可以提炼为:

  1. Default-deny:拒绝默认。macOS 的 seatbelt_readonly_policy.sbpl 第一行就是 (deny default),再选择性 allow,这是 Chrome 沙盒同款思路。
  2. Least Privilege:最小权限。即便是 workspace-write 也不是 "整个 workspace 全开",.git/ / .codex/ / .agents/ 这些"自指目录"被强制只读,避免模型篡改沙盒配置或 git hooks 自我提权。
  3. Fail-closed with Escalation:失败关闭、可升级。沙盒拒绝命令时不静默吞掉,而是上抛一个 ExecApprovalRequest,让用户决策"要不要脱掉沙盒重试"——既保留生产力,又把决策权显式交给人。

1.3 Codex 沙盒 vs 容器化方案

很多人会问"为什么不直接用 Docker 把 Codex 关起来?"答案藏在工程现实里:

维度容器化(Docker / VM)Codex 的进程级沙盒
启动开销秒级微秒级(fork + apply policy)
粒度整个 Codex 进程,包括 IDE 通信仅 LLM 生成的子命令
用户体验每个 shell 调用都跨容器边界与本地开发体感一致(同一 cwd、同一 git state)
逃逸风险内核漏洞 + Docker socket直接落到 LSM/Seatbelt 内核机制
兼容性需要 Docker daemon零依赖(系统自带)

Codex 选了第二条路:进程级沙盒 + LLM 调用粒度。这意味着每次 LLM 调一次 shell,Codex 才 fork 出一个被 Seatbelt/Landlock 包裹的子进程,子进程结束沙盒立刻消亡。


2. 概念分层:Sandbox + Approval 双轴模型

理解 Codex 安全模型的关键是分清两个正交的概念:

2.1 二维矩阵:常见预设组合

来自 Codex 官方文档

意图CLI flag等价于适用场景
Auto / --full-auto无 flag 或 --full-auto--sandbox workspace-write --ask-for-approval on-request日常开发(默认推荐)
只读浏览--sandbox read-only --ask-for-approval on-request代码审查 / 计划讨论
CI 只读--sandbox read-only -a never流水线自动化分析
写 + 慎执行--sandbox workspace-write -a untrusted边写边谨慎运行
🔥 YOLO 模式--dangerously-bypass-approvals-and-sandbox(别名 --yolo极不推荐,仅用于一次性脚本

2.2 启动时智能选择

Codex 启动时会根据 cwd 状态做"动态默认":

  • 是 git repoAuto(workspace-write + on-request)
  • 不是 git repo / 用户未明确 trustread-only,等待用户用 /permissions 显式信任

这条逻辑的潜台词是:git 仓库本身就是回滚保险——即便 LLM 把 workspace 写花了,git reset --hard 一键还原。没有 git 时,这条保险不在,所以默认更保守。


3. SandboxPolicy 的演进:从扁平枚举到结构化策略

3.1 早期版本(trpc-codex / 0.x 形态):4 项扁平枚举

来自本地代码 codex-rs/core/src/protocol.rs

trpc-codex/codex-rs/core/src/protocol.rsL98–L108
rust
pub enum SandboxPolicy {
    /// Network syscalls will be blocked
    NetworkRestricted,
    /// Filesystem writes will be restricted
    FileWriteRestricted,
    /// Network and filesystem writes will be restricted
    #[default]
    NetworkAndFileWriteRestricted,
    /// No restrictions; full "unsandboxed" mode
    DangerousNoRestrictions,
}

这版的设计粗糙之处在于:

  • 维度未解耦:网络限制和文件限制虽然是两个 bool,却被组合成了 4 个枚举值;
  • 没有 writable_roots 概念:所有 Linux 沙盒下的写路径都是从 cwd + $HOME/.pyenv + tempdir 硬编码出来的;
  • 没有 read-only 子路径保护.git/ 完全暴露在 writable_roots 中。

3.2 当前版本(v0.2.0-alpha+):结构化、组合化的策略

上游 openai/codex 主分支SandboxPolicy 已演进为:

rust
pub enum SandboxPolicy {
    ReadOnly {
        access: FileSystemReadAccess,
        network_access: NetworkAccess,
    },
    WorkspaceWrite {
        writable_roots: Vec<PathBuf>,
        read_only_access: FileSystemReadAccess,
        network_access: NetworkAccess,
        exclude_tmpdir_env_var: bool,
        exclude_slash_tmp: bool,
    },
    DangerFullAccess,
    ExternalSandbox {
        network_access: NetworkAccess,
    },
}

四个变体的语义跃迁:

变体文件系统网络关键不变量
ReadOnly全文件系统只读默认 off适合 CI/CR 类纯查阅
WorkspaceWritewritable_roots 内可写,.git/ .codex/ .agents/ 强制只读默认 off,可 opt-in默认值;workspace=cwd + /tmp + $TMPDIR
DangerFullAccess直接 SandboxType::None,不进沙盒不限--yolo 专用
ExternalSandbox由外层(如 cloud container)管仅暴露 network_access用于 Codex Cloud 这类已经在容器里的环境

3.3 关键不变量:保护 .git/ .codex/ .agents/

这是个"看似小,实则关键"的细节。在 WorkspaceWrite 模式下,即使 cwd 整体可写,下列子路径仍被强制划为只读

路径为什么必须只读
**/.git/防止 LLM 改 git hooks(如 pre-commit)实现持久化后门,或者改 config 偷偷加 core.editor
**/.codex/防止 LLM 改 sandbox/approval 配置自我提权(sandbox_mode = "danger-full-access" 的攻击)
**/.agents/防止 LLM 篡改 subagent 角色定义(这是 0.115+ 才加的保护)
gitdir: 指向的真实目录处理 .git 是 pointer 文件的场景

官方文档原文

/.git is protected as read-only whether it appears as a directory or file. If /.git is a pointer file (gitdir: ...), the resolved Git directory path is also protected as read-only.

这是教科书级的"自指攻击防御"——LLM 不能修改控制它自己的那张配置。

3.4 决策入口:assess_command_safety

来自本地 codex-rs/core/src/safety.rs

trpc-codex/codex-rs/core/src/safety.rsL65–L104
rust
pub fn assess_command_safety(
    command: &[String],
    approval_policy: AskForApproval,
    sandbox_policy: SandboxPolicy,
    approved: &HashSet<Vec<String>>,
) -> SafetyCheck {
    let approve_without_sandbox = || SafetyCheck::AutoApprove {
        sandbox_type: SandboxType::None,
    };

    // Previously approved or allow-listed commands
    if is_known_safe_command(command) || approved.contains(command) {
        return approve_without_sandbox();
    }

    // Command was not known-safe or allow-listed
    match sandbox_policy {
        SandboxPolicy::DangerousNoRestrictions => approve_without_sandbox(),
        _ => match get_platform_sandbox() {
            Some(sandbox_type) => SafetyCheck::AutoApprove { sandbox_type },
            None => {
                match approval_policy {
                    AskForApproval::Never => SafetyCheck::Reject {
                        reason: "auto-rejected by user approval settings".to_string(),
                    },
                    _ => SafetyCheck::AskUser,
                }
            }
        },
    }
}

可视化为决策树:

is_known_safe_command 是一个内置 allow-list(ls / cat / sed / pwd / head 等纯读命令),它们走 SandboxType::None 是为了避免不必要的沙盒开销——在 Codex 的世界里,"安全的命令直接放行"和"可疑的命令进沙盒"是两条不同的快速路径


4. 平台实现详解

4.1 macOS:Seatbelt(sandbox-exec

4.1.1 为什么是 Seatbelt

Seatbelt 是苹果自家用来给 Safari/Chrome 做沙盒的机制,它的优势:

  • 零安装/usr/bin/sandbox-exec 系统自带;
  • 声明式策略语言(SBPL,Scheme 方言):表达力够强,能玩 deny-default + 选择性 allow;
  • 路径参数化(subpath (param "WRITABLE_ROOT_0")) 这样的写法允许同一份策略文本配不同 cwd 复用。

但 Seatbelt 也有臭名昭著的"未文档化"特性,所以 Codex 直接抄了 Chrome 的策略骨架。

4.1.2 基础策略文本

来自本地 codex-rs/core/src/seatbelt_readonly_policy.sbpl

trpc-codex/codex-rs/core/src/seatbelt_readonly_policy.sbplL1–L21
txt
(version 1)

; inspired by Chrome's sandbox policy:
; https://source.chromium.org/chromium/chromium/src/+/main:sandbox/policy/mac/common.sb;l=273-319;drc=7b3962fe2e5fc9e2ee58000dc8fbf3429d84d3bd

; start with closed-by-default
(deny default)

; allow read-only file operations
(allow file-read*)

; child processes inherit the policy of their parent
(allow process-exec)
(allow process-fork)
(allow signal (target self))

(allow file-write-data
  (require-all
    (path "/dev/null")
    (vnode-type CHARACTER-DEVICE)))

核心要素:

规则含义
(deny default)默认拒绝一切
(allow file-read*)允许读全文件系统(这是 read-only mode 的本质)
(allow process-exec) (allow process-fork)允许子进程,且子进程继承策略
(allow file-write-data ...)/dev/null写只允许 /dev/null
(allow sysctl-read ...) 一长串 hw.* kern.*Python multiprocessing / 进程信息读取必需

4.1.3 命令拼装

来自本地 codex-rs/core/src/exec.rs::create_seatbelt_command

trpc-codex/codex-rs/core/src/exec.rsL163–L203
rust
pub fn create_seatbelt_command(
    command: Vec<String>,
    sandbox_policy: SandboxPolicy,
    writable_roots: &[PathBuf],
) -> Vec<String> {
    let (policies, cli_args): (Vec<String>, Vec<String>) = writable_roots
        .iter()
        .enumerate()
        .map(|(index, root)| {
            let param_name = format!("WRITABLE_ROOT_{index}");
            let policy: String = format!("(subpath (param \"{param_name}\"))");
            let cli_arg = format!("-D{param_name}={}", root.to_string_lossy());
            (policy, cli_arg)
        })
        .unzip();

    let full_policy = if policies.is_empty() {
        MACOS_SEATBELT_READONLY_POLICY.to_string()
    } else {
        let scoped_write_policy = format!("(allow file-write*\n{}\n)", policies.join(" "));
        format!("{MACOS_SEATBELT_READONLY_POLICY}\n{scoped_write_policy}")
    };

    let mut seatbelt_command: Vec<String> = vec![
        MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string(),
        "-p".to_string(),
        full_policy.to_string(),
    ];
    seatbelt_command.extend(cli_args);
    seatbelt_command.push("--".to_string());
    seatbelt_command.extend(command);
    seatbelt_command
}

最终拼出来形如:

bash
/usr/bin/sandbox-exec -p '(version 1)
(deny default)
(allow file-read*)
...
(allow file-write*
(subpath (param "WRITABLE_ROOT_0"))
)' \
  -DWRITABLE_ROOT_0=/Users/me/projects/foo \
  -- bash -c "echo blah > foo.txt"

4.1.4 当前版本的增强(v0.115+)

在新版本中还做了几件事:

  1. Read-only 子路径排除

    scheme
    (allow file-write*
      (require-all
        (subpath (param "WRITABLE_ROOT_0"))
        (require-not (subpath (param "WRITABLE_ROOT_0_RO_0")))  ; .git
        (require-not (subpath (param "WRITABLE_ROOT_0_RO_1")))  ; .codex
      )
    )
  2. 网络精细化:当 network_access=true 时,加载 seatbelt_network_policy.sbpl,开放 mach-lookup 给 DNS / SecurityServer / trustd / SystemConfiguration。

  3. 代理路由模式:当配置了 network proxy,只允许出站到 localhost:<proxy_port>

    scheme
    (allow network-outbound (remote ip "localhost:43128"))

    这种"必须穿过代理"的设计可以让企业版做流量审计。

  4. 硬编码路径防 PATH 注入

    rust
    const MACOS_PATH_TO_SEATBELT_EXECUTABLE: &str = "/usr/bin/sandbox-exec";

    注释解释得很清楚:

    to defend against an attacker trying to inject a malicious version on the PATH. If /usr/bin/sandbox-exec has been tampered with, then the attacker already has root access.

4.1.5 已知坑:seatbelt + network_access 配置失效

参见 Issue #10390:在 dynamic_network_policy_for_network() 里,enforce_managed_network 的判定混入了 proxy 配置探测,导致用户显式 network_access = true 在 macOS 上被静默忽略。修复版的逻辑:

场景修复前修复后
有可用 proxy走 proxy走 proxy(不变)
无 proxy + network_access=true静默空策略 → 全屏蔽全网放开 + tracing::warn!
无 proxy + network_access=false屏蔽(fail-closed)屏蔽(不变)

启示:沙盒策略一定要 fail-loud,静默拒绝是用户体验灾难。


4.2 Linux:Landlock + seccomp(新版叠加 bubblewrap)

4.2.1 早期形态:Landlock + seccomp(同进程内安装规则)

本地 codex-rs/core/src/linux.rs 展示了最早期的实现:在子线程里直接调 Landlock + seccomp,再 exec() 命令——这是"进程级"沙盒。

关键三步:

(a) Landlock 限制文件系统

trpc-codex/codex-rs/core/src/linux.rsL81–L107
rust
pub fn install_filesystem_landlock_rules_on_current_thread(
    writable_roots: Vec<PathBuf>,
) -> Result<()> {
    let abi = ABI::V5;
    let access_rw = AccessFs::from_all(abi);
    let access_ro = AccessFs::from_read(abi);

    let mut ruleset = Ruleset::default()
        .set_compatibility(CompatLevel::BestEffort)
        .handle_access(access_rw)?
        .create()?
        .add_rules(landlock::path_beneath_rules(&["/"], access_ro))?
        .add_rules(landlock::path_beneath_rules(&["/dev/null"], access_rw))?
        .set_no_new_privs(true);

    if !writable_roots.is_empty() {
        ruleset = ruleset.add_rules(landlock::path_beneath_rules(&writable_roots, access_rw))?;
    }

    let status = ruleset.restrict_self()?;

    if status.ruleset == landlock::RulesetStatus::NotEnforced {
        return Err(CodexErr::Sandbox(SandboxErr::LandlockRestrict));
    }

    Ok(())
}

逻辑:

  1. 默认 / 全只读;
  2. /dev/null 可写;
  3. writable_roots 可写;
  4. set_no_new_privs(true) 禁止 setuid/file capabilities 提权;
  5. restrict_self() 应用规则到当前线程。

Landlock 是单调的:规则一旦应用,只能更严,不能更松。这意味着即便 LLM 调起一个被沙盒包裹的 shell,在 shell 里再 fork 子进程,子进程依然继承沙盒——不存在子进程逃逸

(b) seccomp 屏蔽网络 syscall

trpc-codex/codex-rs/core/src/linux.rsL111–L167
rust
pub fn install_network_seccomp_filter_on_current_thread() -> std::result::Result<(), SandboxErr> {
    let mut rules: BTreeMap<i64, Vec<SeccompRule>> = BTreeMap::new();

    let mut deny_syscall = |nr: i64| { rules.insert(nr, vec![]); };
    deny_syscall(libc::SYS_connect);
    deny_syscall(libc::SYS_accept);
    deny_syscall(libc::SYS_accept4);
    deny_syscall(libc::SYS_bind);
    deny_syscall(libc::SYS_listen);
    // ... 一长串 sendto / recvfrom / sendmsg / recvmsg / setsockopt / ptrace ...

    // 仅允许 socket(AF_UNIX, ...)
    let unix_only_rule = SeccompRule::new(vec![SeccompCondition::new(
        0, SeccompCmpArgLen::Dword, SeccompCmpOp::Eq, libc::AF_UNIX as u64,
    )?])?;
    rules.insert(libc::SYS_socket, vec![unix_only_rule]);

    let filter = SeccompFilter::new(
        rules,
        SeccompAction::Allow,                     // 默认放行
        SeccompAction::Errno(libc::EPERM as u32), // 命中规则返回 EPERM
        ...
    )?;

    let prog: BpfProgram = filter.try_into()?;
    apply_filter(&prog)?;
    Ok(())
}

要点:

解释
connect/accept/bind/listen 全部 deny切断 TCP/UDP socket
socket(AF_UNIX, ...) 单独允许保留 IPC 能力(DBus、X11 socket)
ptrace deny防止子进程 attach 主进程 dump 内存
默认 Allow白名单太长,所以反过来用黑名单

(c) 测试用例佐证

源码里测试了一堆攻击:

rust
sandbox_blocks_curl   // curl http://...
sandbox_blocks_wget   // wget ...
sandbox_blocks_ping   // ICMP raw socket
sandbox_blocks_nc     // nc -z 127.0.0.1 80
sandbox_blocks_ssh    // BatchMode 防止挂起
sandbox_blocks_getent // DNS 查询
sandbox_blocks_dev_tcp_redirection  // bash 的 /dev/tcp 后门

这些是 LLM 越狱时的常见姿势——封到这个程度算彻底。

4.2.2 当前形态:bubblewrap + Landlock + seccomp(外部沙盒进程)

官方文档说明:

Linux uses bwrap plus seccomp by default. WSL1 was supported through Codex 0.114; starting in 0.115, the Linux sandbox moved to bwrap.

这次架构升级把沙盒从"同进程线程上的 LSM"变成了"独立的 helper 进程":

为什么换成 bubblewrap?

老方案(同进程 LSM)新方案(bwrap helper)
沙盒规则跑在 Codex 主进程的子线程独立 helper 进程,主进程零污染
不能玩 mount namespace(/proc /sys 都能看到)bwrap 可以 bind mount + tmpfs 隔离
子进程能看见主进程文件描述符helper 起新 namespace,FD 隔离
配置需要重新编译helper 接受 JSON CLI 参数,独立可调试

新版的 helper 调用形式(来自 上游源码):

bash
codex-linux-sandbox \
  --sandbox-policy-cwd /home/user/proj \
  --command-cwd /home/user/proj \
  --sandbox-policy '{"WorkspaceWrite":{"writable_roots":[...],...}}' \
  --file-system-sandbox-policy '{"kind":"Workspace",...}' \
  --network-sandbox-policy '{"Restricted":...}' \
  --use-legacy-landlock \
  -- npm test

把策略以 JSON 形式塞进 helper,是为了支持滚动升级——主进程可以是新协议,helper 可以是老协议,只要 JSON 字段兼容。

4.2.3 已知坑:sandbox_permissions 读权限未实施

参见 Issue #11316:当前 Landlock 实施分支只做了"写限制",读限制是 TODO——无论怎么配置,沙盒里都能读 /etc/passwd。这在多租户 / agent-of-agent 场景里会成为读侧信道。计划是后续加 readable_roots 字段。


4.3 Windows:Restricted Token + WSL2

子场景沙盒方案
WSL2 (Linux)复用 Linux 的 bwrap + Landlock + seccomp
WSL1在 0.114 之前用 Linux 路径,0.115 起不再支持(因为 WSL1 不支持 bwrap)
Native Windowscodex-windows-sandbox crate,使用 Restricted Token(类似 Chromium 的 SetTokenInformation

Restricted Token 的本质是用 Windows API 创建一个权限被剥夺的访问令牌,启动子进程时让它继承这个令牌。这条路径文档相对少,工程上仍在演进。


5. 执行流程:从 LLM 工具调用到沙箱化执行

把所有片段串起来看一次完整时序:

代码层的核心调度在 codex-rs/core/src/codex.rs::handle_function_call

trpc-codex/codex-rs/core/src/codex.rsL802–L928
rust
async fn handle_function_call(
    sess: &Session,
    sub_id: String,
    name: String,
    arguments: String,
    call_id: String,
) -> ResponseInputItem {
    match name.as_str() {
        "container.exec" | "shell" => {
            let params = serde_json::from_str::<ExecParams>(&arguments)?;

            // 路径 1: apply_patch
            match maybe_parse_apply_patch_verified(&params.command) {
                MaybeApplyPatchVerified::Body(changes) => {
                    return apply_patch(sess, sub_id, call_id, changes).await;
                }
                ...
            }

            // 路径 2: shell 命令
            let safety = assess_command_safety(
                &params.command,
                sess.approval_policy,
                sess.sandbox_policy,
                &state.approved_commands,
            );

            let sandbox_type = match safety {
                SafetyCheck::AutoApprove { sandbox_type } => sandbox_type,
                SafetyCheck::AskUser => {
                    let rx_approve = sess.request_command_approval(...).await;
                    match rx_approve.await {
                        ReviewDecision::Approved => (),
                        ReviewDecision::ApprovedForSession => {
                            sess.add_approved_command(params.command.clone());
                        }
                        Denied | Abort => return reject_response(),
                    }
                    SandboxType::None  // 用户已批,无需沙盒
                }
                SafetyCheck::Reject { reason } => return reject_response(reason),
            };

            let output = process_exec_tool_call(
                params, sandbox_type,
                &writable_roots, sess.ctrl_c.clone(),
                sess.sandbox_policy,
            ).await;
            ...
        }
        _ => unsupported_call_response(),
    }
}

5.1 Resource Capping:output 长度限制

exec.rs 里的 read_capped 限制每路 stdout/stderr 的输出:

上限设计意图
MAX_STREAM_OUTPUT10 KiB防止模型上下文被命令输出灌爆
MAX_STREAM_OUTPUT_LINES256 行行级截断,避免一行 1MB 的 dump
DEFAULT_TIMEOUT_MS10s默认超时,模型可在 ExecParams 里覆盖

超出限制后,继续读但丢弃——这避免了 child 因 pipe 反压挂死。

5.2 SIGKILL / Timeout / Ctrl+C 三态

rust
let exit_status = tokio::select! {
    result = tokio::time::timeout(timeout, child.wait()) => {
        match result {
            Ok(Ok(s)) => s,
            Err(_) => {
                child.start_kill()?;
                synthetic_exit_status(128 + TIMEOUT_CODE)  // 64
            }
        }
    }
    _ = ctrl_c.notified() => {
        child.start_kill()?;
        synthetic_exit_status(128 + SIGKILL_CODE)  // 9
    }
};

synthetic_exit_status 把超时/中断映射为合成的 ExitStatus,让上层用同一套 match raw_output.exit_status.signal() 处理。


6. Escalation 机制:沙盒失败 → 询问 → 不带沙盒重跑

这是 Codex 区别于 Claude Code 等竞品的核心差异化设计

6.1 触发条件

process_exec_tool_call 里这段判定:

trpc-codex/codex-rs/core/src/exec.rsL140–L147
rust
// NOTE(ragona): This is much less restrictive than the previous check. If we exec
// a command, and it returns anything other than success, we assume that it may have
// been a sandboxing error and allow the user to retry. (The user of course may choose
// not to retry, or in a non-interactive mode, would automatically reject the approval.)
if exit_code != 0 && sandbox_type != SandboxType::None {
    return Err(CodexErr::Sandbox(SandboxErr::Denied(
        exit_code, stdout, stderr,
    )));
}

这个设计有意放宽——不去精确识别"这次失败是不是沙盒导致的",因为:

  1. Seatbelt/Landlock 拒绝后,进程通常返回 EPERM,但 EPERM 在用户代码里也可能是别的原因;
  2. 精确识别 = 维护一个"什么 exit_code 对应沙盒"的脆弱白名单;
  3. 倒不如统一处理:只要带沙盒跑挂了,就给用户一个"不带沙盒重试"的选择

代价是:偶尔会问用户"这是 npm test 真的失败了,还是沙盒拒了?"——但这是可接受的代价。

6.2 Escalation 决策状态机

代码体现:

trpc-codex/codex-rs/core/src/codex.rsL986–L1019
rust
match rx_approve.await.unwrap_or_default() {
    ReviewDecision::Approved | ReviewDecision::ApprovedForSession => {
        sess.add_approved_command(params.command.clone());
        sess.notify_background_event(&sub_id, "retrying command without sandbox").await;

        let retry_call_id = format!("{call_id}-retry");
        sess.notify_exec_command_begin(...).await;

        // 关键:SandboxType::None
        let retry_output_result = process_exec_tool_call(
            params.clone(),
            SandboxType::None,
            &retry_roots,
            sess.ctrl_c.clone(),
            sess.sandbox_policy,
        ).await;
        ...
    }
    ReviewDecision::Denied | ReviewDecision::Abort => {
        return reject_response("exec command rejected by user");
    }
}

6.3 设计反思

这个机制很聪明,但也有隐含 bug 嫌疑——代码注释自己都嘀咕:

rust
// TODO(ragona): Isn't this a bug? It always saves the command in an | fork?
sess.add_approved_command(params.command.clone());

意思是:用户选 "Approved"(仅本次)和 "ApprovedForSession"(永久)走的是同一分支,都被加入了 approved_commands HashSet。这看起来违反了"一次"的语义。新版应已修复。


7. 关键安全细节

7.1 主进程加固(Process Hardening)

codex-rs/process-hardening crate 用 #[ctor::ctor]main() 之前跑:

平台加固项防御什么
macOSptrace(PT_DENY_ATTACH)防 LLDB attach 内存读取
macOSsetrlimit(RLIMIT_CORE, 0)禁 core dump,避免 token 落盘
macOS删除所有 DYLD_* 环境变量防 dylib 注入
Linuxprctl(PR_SET_DUMPABLE, 0)进程 non-dumpable,防 ptrace
Linuxsetrlimit(RLIMIT_CORE, 0)同上
Linux删除所有 LD_* 环境变量LD_PRELOAD 注入

注意:这是给 Codex 自己用的,不是给沙盒里的子进程。意图是防止"窃取主进程内存里的 OpenAI token / OAuth refresh_token"。

7.2 子进程的环境变量信号

rust
pub const CODEX_SANDBOX_ENV_VAR: &str = "CODEX_SANDBOX";
pub const CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR: &str = "CODEX_SANDBOX_NETWORK_DISABLED";

子进程通过环境变量知道自己在沙盒里:

变量取值含义
CODEX_SANDBOX"seatbelt" / "landlock" / "bwrap"当前沙盒类型
CODEX_SANDBOX_NETWORK_DISABLED"1" / 不存在网络是否被屏蔽

应用:脚本可以 if [ -n "$CODEX_SANDBOX" ]; then ... 做条件分支(比如跳过需要联网的测试)。

7.3 .env 安全:禁止 CODEX_* 前缀

~/.codex/.env 启动时被 dotenvy 加载,但有过滤:

rust
const ILLEGAL_ENV_VAR_PREFIX: &str = "CODEX_";

防止用户(或被 LLM 诱导写入 .env)通过环境变量绕过沙盒——比如设置 CODEX_SANDBOX=disabled 是不行的,因为 .env 加载时这种变量会被丢弃。

7.4 Auth 文件权限

auth.json 写入时强制 0o600

rust
#[cfg(unix)]
{
    options.mode(0o600);
}

凭据落盘是最常见的"侧信道"。0o600 + macOS Keychain 双保险。

7.5 Allow-list:is_known_safe_command

不进沙盒的"白名单"包括(来自 is_safe_command.rs):

命令族例子
文件浏览ls, pwd, cat, head, tail, wc, file
文本搜索grep, rg, find (limited args)
元信息stat, du, df, which
Git 只读git status, git log, git diff (受 execpolicy 校验)

这些命令哪怕在 read-only 模式也走 SandboxType::None,性能更优。但任何带 shell metacharacter 的版本(ls && rm -rf /)都不会被识别——is_known_safe_command 是 argv 级别的精确匹配。

7.6 execpolicy crate:参数级安全策略

codex-rs/execpolicy 是个独立 crate,提供"特定命令的特定参数模式"安全判定。比如:

  • sed -i 's/foo/bar/' file.txt 是写操作,不安全;
  • sed 's/foo/bar/' file.txt 是读操作,安全。

这种参数级判定让 allow-list 不至于过粗("sed 不行")也不过细("任意 sed 都行")。


8. 与 Claude Code 等竞品的对比

维度Codex CLIClaude CodeAider
沙盒粒度LLM 工具调用粒度LLM 工具调用粒度无(依赖用户审批)
OS 级隔离✅ Seatbelt / Landlock+seccomp / bwrap⚠️ Bash tool 内有 timeout,但无 LSM
写边界writable_roots + 自动 .git/ 保护类似(permissions.toml)
网络默认关闭关闭取决于 shell tool
网络代理路由network-proxy MITM
沙盒失败 escalation✅ 询问用户重试❌ 直接报错N/A
进程加固✅ ptrace deny + 核心 dump 禁用 + DYLD/LD 清洗❓ 未公开
跨平台macOS/Linux/Windows/WSL2macOS/Linux/Windows全平台(无沙盒)
Cloud 模式✅ Codex Cloud(双相执行)⚠️ Claude Computer Use

Codex 的护城河主要在两点:

  1. OS 级原生沙盒——不是脚本层 confirm 弹窗,而是真正落到内核 LSM;
  2. 失败可恢复——沙盒出错不是 dead end,而是 escalation。

9. 关键代码地图

按读源码的优先级排序(基于本地 trpc-codex/codex-rs/):

文件作用阅读优先级
core/src/protocol.rsSandboxPolicy AskForApproval Op EventMsg 枚举定义⭐⭐⭐
core/src/safety.rsassess_command_safety assess_patch_safety get_platform_sandbox⭐⭐⭐
core/src/exec.rsprocess_exec_tool_call create_seatbelt_command read_capped⭐⭐⭐
core/src/linux.rsinstall_filesystem_landlock_rules_on_current_thread, install_network_seccomp_filter_on_current_thread⭐⭐⭐
core/src/seatbelt_readonly_policy.sbplmacOS Seatbelt 基础策略⭐⭐
core/src/codex.rshandle_function_call 主调度 + escalation 流程⭐⭐⭐
cli/src/seatbelt.rscodex debug seatbelt 子命令实现
cli/src/landlock.rscodex debug landlock 子命令实现
core/src/is_safe_command.rsallow-list 实现⭐⭐
execpolicy/参数级命令安全策略⭐⭐
core/src/approval_mode_cli_arg.rsCLI 参数 ↔ Policy 枚举映射

新版上游还需关注:

  • core/src/sandboxing/mod.rsSandboxManager, sandbox_policy_with_additional_permissions
  • core/src/tools/sandboxing.rsSandboxable ToolRuntime SandboxAttempt traits
  • linux-sandbox/:独立的 codex-linux-sandbox helper crate
  • network-proxy/:基于 rama 的 MITM 代理

10. 演进时间线

版本关键变化
0.x(trpc-codex 形态)SandboxPolicy 是扁平的 4 项枚举;Linux 沙盒在主进程子线程内 apply;macOS 仅有 readonly 策略,writable_roots 用 -D 参数注入
0.114WSL1 最后支持版本
0.115Linux 沙盒迁移到 bubblewrap helper 进程SandboxPolicy 演进为结构化枚举(ReadOnly / WorkspaceWrite / DangerFullAccess / ExternalSandbox);.git/ .codex/ .agents/ 强制只读子路径生效
0.117+引入 Subagent 体系,每个子 agent 可独立配置 sandbox_mode;引入 granular approval policy 细分 5 类批准
0.120+引入 auto_review 自动审批 reviewer agent;macOS 网络代理路由稳定
v0.2.0-alpha新增 network-proxy 基于 rama 的 MITM;引入命名 permission profiles 支持 glob 级 read deny;引入 cloud 双相运行时(setup phase + agent phase)

预测的下一步:

  • Landlock 读限制会真正实施(#11316);
  • macOS Seatbelt 配置 source-of-truth 统一(#10390 修复延伸);
  • Windows 沙盒会从 Restricted Token 演进到 Win32 App Container。

11. 实践建议

11.1 默认就用 --full-auto,不要 --yolo

workspace-write + on-request 已经够日常用:

  • 沙盒挡住 ~/.ssh 等敏感路径;
  • 命令失败有 escalation,不至于卡死;
  • 网络默认关,避免无意识的 prompt injection 外发数据。

--yolo 仅用于一次性、无重要数据的临时容器。

11.2 在 CI 上使用 read-only --ask-for-approval never

bash
codex --sandbox read-only -a never -- "review this PR"

非交互环境下,这是唯一安全的组合:模型只能看不能改,且不会卡在弹窗等待批准。

11.3 给 workspace-write 显式 opt-in 网络

toml
# ~/.codex/config.toml
[sandbox_workspace_write]
network_access = true

并配合 web_search = "cached" 默认值——命中缓存的搜索结果,避开实时页面带来的 prompt injection 风险。

11.4 用 permission profile 精细化拒读敏感文件

toml
default_permissions = "workspace"

[permissions.workspace.filesystem]
":project_roots" = { "." = "write", "**/*.env" = "none" }
glob_scan_max_depth = 3

即便整个 workspace 可读写,*.env 也对 LLM 不可见。

11.5 长跑任务用 Subagent 隔离

Subagent 可以独立配置 sandbox_mode。比如让"危险测试 runner"跑在 read-only 子 agent 里,主 agent 只接收摘要,避免子任务污染主 agent context 的同时,把脏活的能耗也限制掉。

11.6 调试沙盒:codex debug 子命令

bash
# macOS
codex debug seatbelt -s read-only -- ls /

# Linux
codex debug landlock -s network-and-file-write-restricted -- curl example.com
# 应该看到连接被 EPERM 拒绝

这两个子命令直接调用沙盒入口,方便排查策略问题——本地代码 cli/src/seatbelt.rs cli/src/landlock.rs 提供了它们的实现。

11.7 验证沙盒是否生效

用副作用检测:

bash
# 应该被拒
codex --sandbox workspace-write -a never -c sandbox_workspace_write.network_access=false \
  "请运行 curl https://example.com 然后告诉我结果"

# 看 CODEX_SANDBOX 环境变量是否被注入
codex "请运行 env | grep CODEX_SANDBOX"

12. 一个表格收尾:什么时候选什么模式

场景推荐配置理由
本地写代码(默认)--full-auto边界合理 + escalation 兜底
审 PR / 读源码--sandbox read-only -a on-request防止意外修改
CI 自动化--sandbox read-only -a never不可交互
跑数据分析脚本--sandbox workspace-write + network_access=true需要装包/拉数据
试一次性的危险脚本在容器里 --yolo物理隔离即风险隔离
多 agent 并行主 agent workspace-write,子 agent read-only限制脏活影响半径
企业部署MDM 推送 requirements.toml 强制 workspace-write 上限集中管控不可绕过
Codex Cloud默认 ExternalSandbox,setup 阶段联网拉依赖,agent 阶段离线双相隔离

参考资料

官方

源码(上游主分支锚点 d807d44a)

第三方分析

相关