## Why This continues the permissions migration by making legacy config default resolution produce the canonical `PermissionProfile` first. The legacy `SandboxPolicy` projection should stay available at compatibility boundaries, but config loading should not create a legacy policy just to immediately convert it back into a profile. Specifically, when `default_permissions` is not specified in `config.toml`, instead of creating a `SandboxPolicy` in `codex-rs/core/src/config/mod.rs` and then trying to derive a `PermissionProfile` from it, we use `derive_permission_profile()` to create a more faithful `PermissionProfile` using the values of `ConfigToml` directly. This also keeps the existing behavior of `sandbox_workspace_write` and extra writable roots after #19841 replaced `:cwd` with `:project_roots`. Legacy workspace-write defaults are represented as symbolic `:project_roots` write access plus symbolic project-root metadata carveouts. Extra absolute writable roots are still added directly and continue to get concrete metadata protections for paths that exist under those roots. The platform sandboxes differ when a symbolic project-root subpath does not exist yet. * **Seatbelt** can encode literal/subpath exclusions directly, so macOS emits project-root metadata subpath policies even if `.git`, `.agents`, or `.codex` do not exist. * **bwrap** has to materialize bind-mount targets. Binding `/dev/null` to a missing `.git` can create a host-visible placeholder that changes Git repo discovery. Binding missing `.agents` would not affect Git discovery, but it would still create a host-visible project metadata placeholder from an automatic compatibility carveout. Linux therefore skips only missing automatic `.git` and `.agents` read-only metadata masks; missing `.codex` remains protected so first-time project config creation goes through the protected-path approval flow. User-authored `read` and `none` subpath rules keep normal bwrap behavior, and `none` can still mask the first missing component to prevent creation under writable roots. ## What Changed - Adds profile-native helpers for legacy workspace-write semantics, including `PermissionProfile::workspace_write_with()`, `FileSystemSandboxPolicy::workspace_write()`, and `FileSystemSandboxPolicy::with_additional_legacy_workspace_writable_roots()`. - Makes `FileSystemSandboxPolicy::workspace_write()` the single legacy workspace-write constructor so both `from_legacy_sandbox_policy()` and `From<&SandboxPolicy>` include the project-root metadata carveouts. - Removes the no-carveout `legacy_workspace_write_base_policy()` path and the `prune_read_entries_under_writable_roots()` cleanup that was only needed by that split construction. - Adds `ConfigToml::derive_permission_profile()` for legacy sandbox-mode fallback resolution; named `default_permissions` profiles continue through the permissions profile pipeline instead of being reconstructed from `sandbox_mode`. - Updates `Config::load()` to start from the derived profile, validate that it still has a legacy compatibility projection, and apply additional writable roots directly to managed workspace-write filesystem policies. - Updates Linux bwrap argument construction so missing automatic `.git`/`.agents` symbolic project-root read-only carveouts are skipped before emitting bind args; missing `.codex`, user-authored `read`/`none` subpath rules, and existing missing writable-root behavior are preserved. - Adds coverage that legacy workspace-write config produces symbolic project-root metadata carveouts, extra legacy workspace writable roots still protect existing metadata paths such as `.git`, and bwrap skips missing `.git`/`.agents` project-root carveouts while preserving missing `.codex` and user-authored missing subpath rules. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19772). * #19776 * #19775 * #19774 * #19773 * __->__ #19772
codex-linux-sandbox
This crate is responsible for producing:
- a
codex-linux-sandboxstandalone executable for Linux that is bundled with the Node.js version of the Codex CLI - a lib crate that exposes the business logic of the executable as
run_main()so that- the
codex-execCLI can check if its arg0 iscodex-linux-sandboxand, if so, execute as if it werecodex-linux-sandbox - this should also be true of the
codexmultitool CLI
- the
On Linux, Codex prefers the first bwrap found on PATH
outside the current working directory whenever it is available. If bwrap is
present but too old to support
--argv0, the helper keeps using system bubblewrap and switches to a
no---argv0 compatibility path for the inner re-exec. If bwrap is missing,
the helper falls back to the vendored bubblewrap path compiled into this
binary.
Codex also surfaces a startup warning when bwrap is missing so users know it
is falling back to the vendored helper. Codex surfaces the same startup warning
path when bubblewrap cannot create user namespaces. WSL2 follows the normal
Linux bubblewrap path. WSL1 is not supported for bubblewrap sandboxing because
it cannot create the required user namespaces, so Codex rejects sandboxed shell
commands that would enter the bubblewrap path.
Current Behavior
-
Legacy
SandboxPolicy/sandbox_modeconfigs remain supported. -
Bubblewrap is the default filesystem sandbox.
-
If
bwrapis present onPATHoutside the current working directory, the helper uses it. -
If
bwrapis present but too old to support--argv0, the helper uses a no---argv0compatibility path for the inner re-exec. -
If
bwrapis missing, the helper falls back to the vendored bubblewrap path. -
If
bwrapis missing, Codex also surfaces a startup warning instead of printing directly from the sandbox helper. -
If bubblewrap cannot create user namespaces, Codex surfaces a startup warning instead of waiting for a runtime sandbox failure.
-
WSL2 uses the normal Linux bubblewrap path.
-
WSL1 is not supported for bubblewrap sandboxing; Codex rejects sandboxed shell commands that would require the bubblewrap path before invoking
bwrap. -
Legacy Landlock + mount protections remain available as an explicit legacy fallback path.
-
Set
features.use_legacy_landlock = true(or CLI-c use_legacy_landlock=true) to force the legacy Landlock fallback. -
The legacy Landlock fallback is used only when the split filesystem policy is sandbox-equivalent to the legacy model after
cwdresolution. -
Split-only filesystem policies that do not round-trip through the legacy
SandboxPolicymodel stay on bubblewrap so nested read-only or denied carveouts are preserved. -
When bubblewrap is active, the helper applies
PR_SET_NO_NEW_PRIVSand a seccomp network filter in-process. -
When bubblewrap is active, the filesystem is read-only by default via
--ro-bind / /. -
When bubblewrap is active, writable roots are layered with
--bind <root> <root>. -
When bubblewrap is active, protected subpaths under writable roots (for example
.git, resolvedgitdir:, and.codex) are re-applied as read-only via--ro-bind. -
When bubblewrap is active, overlapping split-policy entries are applied in path-specificity order so narrower writable children can reopen broader read-only or denied parents while narrower denied subpaths still win. For example,
/repo = write,/repo/a = none,/repo/a/b = writekeeps/repowritable, denies/repo/a, and reopens/repo/a/bas writable again. -
When bubblewrap is active, unreadable glob entries are expanded before launching the sandbox and matching files are masked in bubblewrap:
Prefer: rg --files --hidden --no-ignore --glob <pattern> -- <search-root> Fallback: internal globset walker when rg is not installed Failure: any other rg failure aborts sandbox constructionUsers can cap the scan depth per permissions profile:
[permissions.workspace.filesystem] glob_scan_max_depth = 2 [permissions.workspace.filesystem.":project_roots"] "**/*.env" = "none" -
When bubblewrap is active, symlink-in-path and non-existent protected paths inside writable roots are blocked by mounting
/dev/nullon the symlink or first missing component. -
When bubblewrap is active, the helper explicitly isolates the user namespace via
--unshare-userand the PID namespace via--unshare-pid. -
When bubblewrap is active and network is restricted without proxy routing, the helper also isolates the network namespace via
--unshare-net. -
In managed proxy mode, the helper uses
--unshare-netplus an internal TCP->UDS->TCP routing bridge so tool traffic reaches only configured proxy endpoints. -
In managed proxy mode, after the bridge is live, seccomp blocks new AF_UNIX/socketpair creation for the user command.
-
When bubblewrap is active, it mounts a fresh
/procvia--proc /procby default, but you can skip this in restrictive container environments with--no-proc.
Notes
- The CLI surface still uses legacy names like
codex debug landlock.