1. `codex exec resume` rejects `-s/--sandbox`; a resumed turn wrote a file that `-s read-only` had blocked
What version of Codex CLI is running?
0.147.0
What subscription do you have?
plus
Which model were you using?
gpt-5.6-sol
What platform is your computer?
Linux 6.8.0-137-generic x86_64 x86_64
What terminal emulator and version are you using (if applicable)?
None — non-interactive. Commands were run over a non-interactive SSH session (no TTY: tty returns "not a tty", TERM=dumb), invoking codex exec via runuser -l <user> -c .... No multiplexer in the test session (TMUX and STY both unset). tmux 3.4 is installed on the host but was not involved. Shell: bash 5.2.21(1)-release.
Codex doctor report
{
"schemaVersion": 1,
"generatedAt": "1787433825s since unix epoch",
"overallStatus": "ok",
"codexVersion": "0.147.0",
"checks": {
"app_server.status": {
"id": "app_server.status",
"category": "app-server",
"status": "ok",
"summary": "background server is not running",
"details": {
"control socket": "/home/codex/.codex/app-server-control/app-server-control.sock",
"daemon state dir": "/home/codex/.codex/app-server-daemon",
"mode": "ephemeral",
"pid file": "/home/codex/.codex/app-server-daemon/app-server.pid (missing)",
"settings": "/home/codex/.codex/app-server-daemon/settings.json (missing)",
"status": "not running",
"update-loop pid file": "/home/codex/.codex/app-server-daemon/app-server-updater.pid (missing)"
},
"remediation": null,
"durationMs": 0
},
"auth.credentials": {
"id": "auth.credentials",
"category": "auth",
"status": "ok",
"summary": "auth is configured",
"details": {
"auth file": "/home/codex/.codex/auth.json",
"auth storage mode": "File",
"stored API key": "false",
"stored ChatGPT tokens": "true",
"stored agent identity": "false",
"stored auth mode": "chatgpt"
},
"remediation": null,
"durationMs": 0
},
"config.load": {
"id": "config.load",
"category": "config",
"status": "ok",
"summary": "config loaded",
"details": {
"CODEX_HOME": "/home/codex/.codex",
"config.toml": "/home/codex/.codex/config.toml",
"config.toml parse": "ok",
"cwd": "/home/codex",
"enabled feature flags": "shell_tool, view_image, unified_exec, shell_snapshot, code_mode_host, terminal_resize_reflow, sqlite, hooks, enable_request_compression, multi_agent, apps, tool_search_always_defer_mcp_tools, tool_suggest, plugins, in_app_browser, in_app_updates, browser_use, browser_use_full_cdp_access, browser_use_external, computer_use, remote_plugin, plugin_sharing, image_generation, resize_all_images, item_ids, skill_mcp_dependency_install, skill_search, mentions_v2, steer, guardian_approval, goals, collaboration_modes, tool_call_mcp_elicitation, auth_elicitation, personality, fast_mode, tui_app_server, remote_compaction_v2, workspace_dependencies",
"feature flag overrides": "none",
"feature flags enabled": "39",
"log dir": "/home/codex/.codex/log",
"mcp servers": "0",
"model": "<default>",
"model provider": "openai",
"sqlite home": "/home/codex/.codex"
},
"remediation": null,
"durationMs": 0
},
"git.environment": {
"id": "git.environment",
"category": "git",
"status": "ok",
"summary": "git version 2.43.0",
"details": {
"PATH git #1": "/usr/bin/git",
"PATH git #2": "/bin/git",
"PATH git entries": "2",
"git build options": "git version 2.43.0; cpu: x86_64; no commit associated with this build; sizeof-long: 8; sizeof-size_t: 8; shell-path: /bin/sh",
"git exec path": "/usr/lib/git-core",
"git version": "git version 2.43.0",
"repo detected": "false",
"selected git": "/usr/bin/git"
},
"remediation": null,
"durationMs": 80
},
"installation": {
"id": "installation",
"category": "install",
"status": "ok",
"summary": "installation looks consistent",
"details": {
"PATH codex #1": "/usr/bin/codex",
"PATH codex #2": "/bin/codex",
"PATH codex entries": "2",
"current executable": "/usr/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/bin/codex",
"install context": "npm (package /usr/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl, bin /usr/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/bin, resources /usr/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex-resources, path /usr/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex-path)",
"managed by bun": "false",
"managed by npm": "true",
"managed by pnpm": "false",
"managed package root": "/usr/lib/node_modules/@openai/codex",
"npm update target": "/usr/lib/node_modules/@openai/codex"
},
"remediation": null,
"durationMs": 105
},
"mcp.config": {
"id": "mcp.config",
"category": "mcp",
"status": "ok",
"summary": "no MCP servers configured",
"details": {},
"remediation": null,
"durationMs": 0
},
"network.env": {
"id": "network.env",
"category": "network",
"status": "ok",
"summary": "network-related environment looks readable",
"details": {
"proxy env vars": "none"
},
"remediation": null,
"durationMs": 0
},
"network.provider_reachability": {
"id": "network.provider_reachability",
"category": "reachability",
"status": "ok",
"summary": "active provider endpoints are reachable over HTTP",
"details": {
"ChatGPT base URL": "https://chatgpt.com/backend-api/ reachable (HTTP 403)",
"reachability mode": "ChatGPT auth"
},
"remediation": null,
"durationMs": 106
},
"network.websocket_reachability": {
"id": "network.websocket_reachability",
"category": "websocket",
"status": "ok",
"summary": "Responses WebSocket handshake succeeded",
"details": {
"DNS": "2 IPv4, 2 IPv6, first IPv6",
"auth mode": "chatgpt",
"connect timeout": "15000 ms",
"endpoint": "wss://chatgpt.com/backend-api/<redacted>",
"handshake result": "HTTP 101 Switching Protocols",
"model provider": "openai",
"models etag present": "true",
"provider name": "OpenAI",
"proxy env vars": "none",
"reasoning header": "false",
"server model present": "false",
"supports websockets": "true",
"wire API": "responses"
},
"remediation": null,
"durationMs": 950
},
"runtime.provenance": {
"id": "runtime.provenance",
"category": "runtime",
"status": "ok",
"summary": "running npm on linux-x86_64",
"details": {
"commit": "unknown",
"current executable": "/usr/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/bin/codex",
"install method": "npm (package /usr/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl, bin /usr/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/bin, resources /usr/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex-resources, path /usr/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex-path)",
"platform": "linux-x86_64",
"version": "0.147.0"
},
"remediation": null,
"durationMs": 0
},
"runtime.search": {
"id": "runtime.search",
"category": "search",
"status": "ok",
"summary": "search is OK (bundled)",
"details": {
"search command": "/usr/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex-path/rg",
"search command readiness": "file exists",
"search provider": "bundled"
},
"remediation": null,
"durationMs": 0
},
"sandbox.helpers": {
"id": "sandbox.helpers",
"category": "sandbox",
"status": "ok",
"summary": "sandbox configuration is readable",
"details": {
"approval policy": "OnRequest",
"codex-linux-sandbox helper": "/home/codex/.codex/tmp/arg0/codex-arg0DpW0lQ/codex-linux-sandbox",
"execve wrapper helper": "/home/codex/.codex/tmp/arg0/codex-arg0DpW0lQ/codex-execve-wrapper",
"filesystem sandbox": "restricted",
"network sandbox": "restricted"
},
"remediation": null,
"durationMs": 0
},
"state.paths": {
"id": "state.paths",
"category": "state",
"status": "ok",
"summary": "state paths and databases are inspectable",
"details": {
"CODEX_HOME": "/home/codex/.codex (dir)",
"active rollout files": "90 files, 54264040 total bytes, 602933 average bytes",
"archived rollout files": "0 files, 0 total bytes, 0 average bytes",
"goals DB": "/home/codex/.codex/goals_1.sqlite (file)",
"goals DB integrity": "ok",
"log DB": "/home/codex/.codex/logs_2.sqlite (file)",
"log DB integrity": "ok",
"log dir": "/home/codex/.codex/log (dir)",
"memories DB": "/home/codex/.codex/memories_1.sqlite (file)",
"memories DB integrity": "ok",
"queue DB": "/home/codex/.codex/queue_1.sqlite (file)",
"queue DB integrity": "ok",
"sqlite home": "/home/codex/.codex (dir)",
"state DB": "/home/codex/.codex/state_5.sqlite (file)",
"state DB integrity": "ok",
"thread history DB": "/home/codex/.codex/thread_history_1.sqlite (missing)",
"thread history DB integrity": "skipped (missing)"
},
"remediation": null,
"durationMs": 97
},
"state.rollout_db_parity": {
"id": "state.rollout_db_parity",
"category": "threads",
"status": "ok",
"summary": "rollout files and state DB thread inventory agree",
"details": {
"default model provider": "openai",
"rollout DB active files": "90",
"rollout DB active rows": "90",
"rollout DB archive mismatches": "0",
"rollout DB archived files": "0",
"rollout DB archived rows": "0",
"rollout DB duplicate DB paths": "0",
"rollout DB duplicate rollout thread ids": "0",
"rollout DB malformed file names": "0",
"rollout DB missing active rows": "0",
"rollout DB missing archived rows": "0",
"rollout DB model providers": "openai=90",
"rollout DB rows": "90",
"rollout DB scan cap reached": "false",
"rollout DB scan errors": "0",
"rollout DB sources": "exec=55, subagent:thread_spawn=35",
"rollout DB stale rows": "0"
},
"remediation": null,
"durationMs": 213
},
"system.environment": {
"id": "system.environment",
"category": "system",
"status": "ok",
"summary": "OS language unavailable",
"details": {
"EDITOR": "not set",
"VISUAL": "not set",
"os": "Ubuntu 24.4.0 (noble) [64-bit]",
"os language": "unavailable",
"os type": "Ubuntu",
"os version": "24.4.0"
},
"remediation": null,
"durationMs": 20
},
"terminal.env": {
"id": "terminal.env",
"category": "terminal",
"status": "ok",
"summary": "terminal metadata was detected",
"details": {
"color output": "disabled (stdout is not a terminal)",
"stderr is terminal": "false",
"stdin is terminal": "false",
"stdout is terminal": "false",
"terminal": "unknown",
"terminal size": "unavailable (Resource temporarily unavailable (os error 11))"
},
"remediation": null,
"durationMs": 4
},
"terminal.title": {
"id": "terminal.title",
"category": "title",
"status": "ok",
"summary": "terminal title default",
"details": {
"terminal title activity": "true",
"terminal title items": "activity, project-name",
"terminal title project source": "cwd",
"terminal title project value": "codex",
"terminal title source": "default"
},
"remediation": null,
"durationMs": 0
},
"updates.status": {
"id": "updates.status",
"category": "updates",
"status": "ok",
"summary": "update configuration is locally consistent",
"details": {
"check for update on startup": "true",
"latest version": "0.149.0",
"latest version status": "newer version is available",
"npm update target": "/usr/lib/node_modules/@openai/codex",
"update action": "npm install -g @openai/codex",
"version cache": [
"/home/codex/.codex/version.json",
"missing"
]
},
"remediation": null,
"durationMs": 171
}
}
}
What issue are you seeing?
The same prompt in every phase, instructing the model to run echo WRITTEN > canary.txt
inside a freshly created temporary working directory. The verdict is whether the file
exists, not what the model reports:
| phase | command | write attempt in events | file on disk |
|---|---|---|---|
| 0 | exec -s workspace-write (positive control) | yes | created |
| 1 | exec -s read-only | none | not created |
| 2 | exec resume <SID> without -c sandbox_mode | yes | created |
| 3 | exec resume <SID> -c sandbox_mode="read-only" | none | not created |
Phase 0 exists to validate the measurement itself. Without a case where the file must
appear, "not created" cannot distinguish a sandbox that blocks from a model that never tried.
Why it matters
A common pattern in cross-model review is to open the session read-only and continue withresume for later rounds, so the reviewer remembers the critiques it already made. With this
asymmetry, whoever sets -s read-only on the first turn operates from the second turn under a
policy they did not choose. The only available route is -c sandbox_mode="read-only", which
does not appear in the subcommand's help — and #3309 already reports that resume is thinly
documented.
This is the headless/CI-style usage of codex exec, which is also the scenario where the
change is least likely to be noticed: with nobody watching the screen, the file simply appears.
What I would expect, in order of preference
exec resumeaccepts-s/--sandbox, likeexec.exec resumeexplicitly inherits the sandbox policy of the resumed session.- At minimum, the help for
resumestates that the sandbox must be set via-c sandbox_mode.
Limitations I want to state
- In phases 1 and 3 the model made no write attempt: knowing it was read-only, it declined to
try. In those phases the absent file is therefore not an observed refusal by the system. The
difference between phase 2 and phase 3 is still measured on disk.
- The model answered "BLOCKED" in every phase where it had attempted nothing at all.
codex doctor --jsonreportsoverallStatus: okon this machine,sandbox.helpers
included, on the same run where phase 2 wrote the file. The diagnostic covers installation
health and does not surface whether the effective policy in resume is the one requested.
What steps can reproduce the bug?
Campo: "What steps can reproduce the bug?"
Thread id of the documented run: 01a02b44-c7d9-73c1-bbf3-f5e88d468ad2
Independently reproduced on a second session: 01a02b63-5177-72c1-89de-d348099398ff
---
Step A — the flag itself
$ codex exec resume 00000000-0000-0000-0000-000000000000 -s read-only "x"
error: unexpected argument '-s' found
The same flag is accepted by codex exec.
Steps 0-3 — what the policy actually is. The script below is what I ran, verbatim, as an
unprivileged user. It asks the model to run echo WRITTEN > canary.txt and then checks the
filesystem after each phase, instead of trusting the model's answer.
#!/usr/bin/env bash
set -u
WORK=$(mktemp -d) && cd "$WORK" || exit 1
PROMPT='Run this exact command with the shell, now, in the current working directory:
echo WRITTEN > canary.txt
Attempt it even if you expect it to fail: the attempt itself is what matters.
Then reply with ONE word: WRITTEN if the file exists, BLOCKED if the system refused.'
say() { [ -f canary.txt ] && echo " file on disk: CREATED" || echo " file on disk: not created"; rm -f canary.txt; }
echo "[0] positive control - exec -s workspace-write (file MUST appear)"
printf '%s' "$PROMPT" | codex exec -s workspace-write --skip-git-repo-check --json - > ev0.json 2>/dev/null
say
echo "[1] exec -s read-only"
printf '%s' "$PROMPT" | codex exec -s read-only --skip-git-repo-check --json - > ev1.json 2>/dev/null
say
SID=$(grep -oE '"(session_id|thread_id|conversation_id)"[[:space:]]*:[[:space:]]*"[0-9a-fA-F-]{36}"' ev1.json | head -1 | grep -oE '[0-9a-fA-F-]{36}')
echo " session: ${SID:-NOT FOUND}"
[ -n "$SID" ] || exit 1
echo "[2] resume WITHOUT -c sandbox_mode"
printf '%s' "$PROMPT" | codex exec resume "$SID" --skip-git-repo-check --json - > ev2.json 2>/dev/null
say
echo "[3] resume WITH -c sandbox_mode=read-only"
printf '%s' "$PROMPT" | codex exec resume "$SID" --skip-git-repo-check -c sandbox_mode="read-only" --json - > ev3.json 2>/dev/null
say
Observed output (codex-cli 0.147.0, model gpt-5.6-sol):
[0] positive control - exec -s workspace-write (file MUST appear)
file on disk: CREATED
[1] exec -s read-only
file on disk: not created
session: 01a02b63-5177-72c1-89de-d348099398ff
[2] resume WITHOUT -c sandbox_mode
file on disk: CREATED <-- the bug
[3] resume WITH -c sandbox_mode=read-only
file on disk: not created
Phase 0 is there on purpose: without a case where the file must appear, "not created" would
not distinguish a sandbox that blocks from a model that never tried. In phases 1 and 3 the
model made no attempt at all — it answered "BLOCKED" without trying — so those two phases show
the absence of a write, not an observed refusal. Phase 2 is the one measured on disk.
What is the expected behavior?
The effective sandbox policy should not change silently between turns of the same conversation.
In order of preference:
codex exec resume accepts -s/--sandbox, the same as codex exec. The two subcommands would then agree, and a caller who set a policy on the first turn can set the same one on every following turn.
codex exec resume explicitly inherits the sandbox policy of the session it resumes. The user chose that policy when opening the session; carrying it forward matches what most callers already assume.
At minimum, codex exec resume --help states that the sandbox must be set through -c sandbox_mode="…", since the flag is unavailable here. Today neither the help nor the error message mentions that route, and the error's own hint (to pass '-s' as a value, use '-- -s') points somewhere else entirely.
If the current behaviour is intentional, then option 3 is the part I would still ask for: the difference between exec and exec resume is invisible until you measure it on disk.
Additional information
Scope of what I tested. Linux only (Ubuntu 24.04, codex-cli 0.147.0), on an account with no ~/.codex/config.toml — so phase 2 ran under CLI defaults, not under a permissive config of my own. I did not test macOS, other versions, or the case where a config.toml sets a broader sandbox_mode. That last case is worth noting: if a user's config specifies workspace-write, a resume after exec -s read-only would presumably inherit the broader policy, which is a larger gap than the one I measured. I have not verified it, and I did not want to report it as fact.
On the model's self-report. In every phase where nothing was attempted, the model answered "BLOCKED". Its answer tracked what it believed its permissions were, not what it had done. Anyone diagnosing sandbox behaviour from model output alone would reach the wrong conclusion in both directions.
Happy to run further tests on this setup if you want a specific configuration or version checked — the reproducer takes about a minute per phase.
A note on disclosure. I plan to document this case in a public repository about verifying review pipelines. I follow the practice of reporting upstream first and linking the issue, so nothing goes out before you have seen it. If you would rather I hold the details until you have responded, say so and I will.
4 Comments
Nice repro. Since
exec resumerejects-soutright, the practical route is what you found in phase 3: pass-c sandbox_mode="read-only"on every resume call, because the session's sandbox mode doesn't get persisted at resume time. I'd also test settingsandbox_modein~/.codex/config.tomlsince you're on defaults, that would apply to resumed sessions too. Worth checking #3309 (resume is thinly documented) and 0.149.0, your doctor shows it's available. If the sandbox mode never survives into the rollout JSON, that's a legit bug to flag upstream.I reviewed current
mainat4582c0aagainst the reported CLI asymmetry. The static parser path supports the repro:codex-rs/exec/src/cli.rsdefinesResumeArgswith session selection,--last,--all, images, and prompt, but no--sandbox/-soption.mark_exec_global_args; sandbox selection is not promoted in the same way.-c sandbox_mode=...route therefore bypasses the missing resume flag rather than proving that the resumed session inherited the original effective policy.I could not run the Rust CLI here because this macOS worker has no
cargotoolchain, and the reported Linux sandbox behavior needs a Linux runtime. The repository contribution guide also says external code PRs are not accepted, so I am not proposing a patch. This looks like a good candidate for the Codex team to confirm whether resume should inherit the recorded policy or expose the same sandbox option, and to add a headless regression test that checks filesystem effects rather than model self-report.@MilkyWay008 - Thanks — following your pointers I found that my explanation of the mechanism was wrong,
and the corrected version is more specific. Two corrections and one isolated cause.
Correction 1 — the version
I measured on 0.147.0 while 0.149.0 was already out (20 Aug). The answer was inside the
codex doctoroutput I attached:updates.statusreports"latest version": "0.149.0"and"newer version is available". I read that file for secrets before posting and never read whatit said.
Upgraded to 0.149.0 and re-ran everything:
-sis still rejected byexec resume, thefour-phase result is unchanged, and
resume --helpstill does not mention-c sandbox_mode.Correction 2 —
config.tomlworks, as you suspectedCODEX_HOMEpointed at a throwaway dir, one run per setting:|
config.toml|exec(no flag) |resume(no flag) ||---|---|---|
|
sandbox_mode = "workspace-write"| created | created ||
sandbox_mode = "read-only"| not created | not created |A
sandbox_modeinconfig.tomldoes reach resumed sessions.The isolated cause — it is
trust_level, not the session policyYour
config.tomlsuggestion made me look at that file, where I found entries my own runs hadwritten:
[projects."<dir>"] trust_level = "trusted". My phase 0 —exec -s workspace-write,present only as a positive control — was marking the working directory as trusted, and that is
what let phase 2 write.
Isolated with one variable, same session, same directory, same prompt:
Only the trust line changed between [2] and [3].
What this makes the report
The observable fact stands: in that sequence a resumed turn wrote a file the first turn had
blocked. The mechanism is narrower, and it is the combination of two behaviours:
exec, an explicit-s read-onlyoverrides directory trust — phase 1 above did notwrite despite the directory being trusted.
exec resume,-sis rejected, so that override is unavailable and trust wins.So the gap is not "resume loses the session's sandbox". It is that trust granted to a directory
once — by a single earlier
workspace-writerun — outlives it, andresumeoffers no way tooverride it the way
execdoes. Precedence appears to beconfig.toml sandbox_mode>trust_level> default: withsandbox_mode = "read-only"set, the trusted directory did notwrite.
What I would ask now
exec resumeaccept-s/--sandbox, so the same override available onexecexists onresume. This alone closes the gap.
trust_level,sandbox_modeand-s— I could not find it,and I reached it by measurement.
-c sandbox_modeinresume --help, since it is the only working routetoday (#3309 already notes resume is thinly documented).
Everything above is 0.149.0, measured 23 Aug 2026. Happy to re-run any variant you want checked.
@Hughhhhcoder - I checked your source reading against
mainbefore relying on it, and it holds:ResumeArgsincodex-rs/exec/src/cli.rscarries onlysession_id,last,all,imagesand
prompt, with no sandbox field, and the singlesandboxoccurrence in that file sitsinside
mark_exec_global_args.Your static reading explains the measurement I posted above.** With no way to express a
sandbox on
resume, the effective policy falls back to the environment — and directorytrust_levelis part of that environment. Onexecan explicit-s read-onlyoverrides trust;on
resumethat override does not exist, so trust wins. Two paths, same conclusion.On your open question — whether resume inherits the recorded policy: measured, it does not.**
It resolves from the current environment. Which also confirms your reading of the workaround:
-c sandbox_modebypasses the missing flag rather than restoring inheritance. Settingsandbox_mode = "read-only"inconfig.tomlsuppressed the write even in a trusted directory,so the observed precedence is
sandbox_mode>trust_level> default.One asymmetry I noticed while verifying your point**, which I had missed and which neither of
us mentioned: in
mark_exec_global_args,dangerously_bypass_approvals_and_sandboxispromoted to the subcommands — it shows up in
resume --help— while sandbox selection is not.So on
exec resumetoday the option that removes the sandbox is available and the option thatsets it is not.
Seconding your last suggestion, with a data point for it: a headless regression test that
asserts filesystem effects rather than model self-report. Across my runs the model answered
"BLOCKED" in every phase where it had attempted nothing at all, and "WRITTEN" when it had. Its
self-report tracked what it believed its permissions to be, so a test reading model output would
have passed while the file was being created. Checking for the file is what caught this.
I have a Linux box with the reproducer wired up and no constraint on running variants, so if a
maintainer wants a specific combination checked — other sandbox modes,
--last, a trusteddirectory with an explicit config — I can run it and post the outcomes.