Windows sandbox: setup payload passed on the command line exceeds the CreateProcess limit (os error 206)

Open 💬 2 comments Opened Aug 17, 2026 by ArsAsier

Environment

  • Windows 11
  • Codex Desktop app 26.810.7004.0 (helper: app\\resources\\codex-windows-sandbox-setup.exe); sandbox bin: codex-command-runner-0.148.0-alpha.9.exe, codex.exe
  • sandbox_mode = "workspace-write", [windows] sandbox = "elevated", LongPathsEnabled = 1

Symptom

Every command executed through the Windows sandbox fails before it even runs:

[codex.exe] setup refresh: spawning ...\\codex-windows-sandbox-setup.exe (cwd=..., payload_len=35044)
[codex.exe] setup refresh: setup refresh failed to launch helper: helper=...\\codex-windows-sandbox-setup.exe, ...
error=The filename or extension is too long. (os error 206)

This is ERROR_FILENAME_EXCED_RANGE returned by CreateProcess: the assembled command line
(helper path + cwd + embedded payload) exceeds Windows' 32,767-character limit.

Repro

  1. Place ~700 loose files directly in the user profile (e.g. C:\Users\<user>\*.sh).
  2. Run any command through the Windows sandbox (default, non-escalated path).
  3. The sandbox "setup refresh" step spawns codex-windows-sandbox-setup.exe with a payload

whose size grows roughly linearly with the top-level item count of the user profile.
Observed in %USERPROFILE%\.codex\.sandbox\sandbox.YYYY-MM-DD.log:

  • ~150 top-level items -> payload_len=15108, helper completes normally
  • ~500 top-level items -> still fine
  • ~700 top-level items -> payload_len=35044, CreateProcess fails with 206;

EVERY sandboxed command fails from then on.

  1. Escalated (non-sandboxed) commands bypass the helper and keep working,

which makes the failure mode hard to diagnose.

Boundary (useful data point)

The threshold lies between 500 and 700 top-level profile items. After moving ~200 items
into a subfolder (697 -> 498 top-level entries) the sandbox recovered immediately on the
next command, no restart required.

Suggested fixes

  • Pass the setup payload to the helper via a temp file or a pipe instead of the command line.
  • Or keep the embedded profile-item list bounded (only roots that actually exist, or a

digest instead of the full flat list).

Logs

%USERPROFILE%\.codex\.sandbox\sandbox.YYYY-MM-DD.log

View original on GitHub ↗

2 Comments

jdcodes1 · 10 days ago

Verified against main @ 1f41cc5d92 — your diagnosis is exactly right, and the code shows both why the payload is unbounded and why it ended up on the command line in the first place.

The two halves of the mechanism:

  1. Unbounded payload: the deny-read configuration is expanded into a flat enumerated list of every matching filesystem pathcollect_existing_glob_matches recursively walks the scan roots and pushes each concrete match:

https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/windows-sandbox-rs/src/deny_read_resolver.rs#L84-L136

With deny globs anchored at the user profile, the list scales with your top-level item count — your ~50-bytes-per-item slope (15,108 chars at ~150 items → 35,044 at ~700) is just path length × base64 overhead.

  1. Payload on argv: the refresh path serializes the whole SetupPayload to JSON, base64-encodes it, and passes it as a single argument to the helper:

https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/windows-sandbox-rs/src/setup.rs#L352-L377

Base64 inflates by 4/3, so the effective JSON budget under the 32,767-char CreateProcess limit is only ~24 KB — a ceiling any large profile can hit, as you demonstrated.

Why argv was chosen (and why the fix is still easy): the initial setup path launches the helper via ShellExecuteExW for UAC elevation (setup.rs#L916+), and ShellExecute can only pass parameters through the command line — no stdin piping. That constraint got inherited by the refresh path, but the refresh explicitly never elevates (the comment at #L373 says so) and spawns via plain Command — so it can switch to a piped stdin write with a very small diff, no temp-file lifetime questions. For the elevated initial path, a temp file whose path is the argv (with the payload’s ACL restricted to the invoking user) fits within limits regardless of profile size.

One more fix worth taking regardless of transport: expanding globs on the orchestrator side and shipping the concrete path list also bakes in a TOCTOU gap — the profile contents can change between enumeration and the helper applying ACLs. Passing the patterns + scan roots and letting the helper expand them in-process both bounds the payload (it becomes O(config), not O(filesystem)) and closes that drift. Your observed recovery-by-reorganizing (697 → 498 items) works precisely because the payload is O(filesystem) today.

Cheap guard for the interim: the spawn site already logs payload_len; a pre-flight check (b64.len() > ~30_000 → fail with "sandbox setup payload exceeds the Windows command-line limit; N deny-list entries — see <doc>") would at least convert the opaque os error 206 into an actionable message, given your point that escalated commands silently bypass the helper and mask the failure.

Regression test shape: construct a payload with ~1,000 synthetic deny paths and assert the chosen transport delivers it (stdin/temp-file round-trip), plus a unit assert that refresh-payload size is independent of the number of concrete glob matches once patterns-not-paths lands.

LittleBitPlanet · 8 hours ago

Additional datapoint from a different trigger path, same error class, on CLI 0.150.1 and 0.151.0-alpha.6 (npm install, Windows 11 26200, [windows] sandbox = "elevated"):

Session INIT (not per-command execution) fails deterministically on write-capable profiles with failed to load AGENTS.md instructions for environment 'local': The filename or extension is too long. (os error 206), before the TUI ever appears. Read-only profiles on the same home are fine.

Two observations that may help narrow the payload contributor:

  1. The user profile root here has only 169 top-level items, far below the ~500-700 boundary in the report above, so the flat profile-item list cannot be the whole payload. This CODEX_HOME is long-established (since March) and its cap_sid has accumulated 90 per-cwd/per-root sandbox SIDs (8,938 bytes); the sandbox log shows setup refresh: spawning codex-windows-sandbox-setup.exe (payload_len=9208) cycles completing, so the spawn that dies with 206 appears to be a later, unlogged helper invocation with a larger assembled command line.
  2. A byte-identical robocopy clone of the same CODEX_HOME under a different path starts fine, but only because the clone silently runs a reduced sandbox flow (its .sandbox-bin never receives a command-runner), which masks the failure and makes fresh-home repro attempts misleading.

Resetting the accumulated state (cap_sid pruned to 6 live entries, .sandbox renamed aside, both together) did NOT clear it here, so the payload contributor is something else that survives a state reset. Filed the startup-walk hang that was masking this as #41257; happy to provide full logs from this machine if useful.