1. `codex exec resume` rejects `-s/--sandbox`; a resumed turn wrote a file that `-s read-only` had blocked

Open 💬 4 comments Opened Aug 22, 2026 by antoniodelia-hash

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 with
resume 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

  1. exec resume accepts -s/--sandbox, like exec.
  2. exec resume explicitly inherits the sandbox policy of the resumed session.
  3. At minimum, the help for resume states 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 --json reports overallStatus: ok on 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

repro.sh

$ 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.

View original on GitHub ↗

4 Comments

MilkyWay008 · 5 days ago

Nice repro. Since exec resume rejects -s outright, 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 setting sandbox_mode in ~/.codex/config.toml since 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.

Hughhhhcoder · 5 days ago

I reviewed current main at 4582c0a against the reported CLI asymmetry. The static parser path supports the repro:

  • codex-rs/exec/src/cli.rs defines ResumeArgs with session selection, --last, --all, images, and prompt, but no --sandbox/-s option.
  • The top-level exec parser promotes only selected shared options to subcommands in mark_exec_global_args; sandbox selection is not promoted in the same way.
  • The existing -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 cargo toolchain, 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.

antoniodelia-hash · 5 days ago

@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 doctor output I attached: updates.status reports "latest version": "0.149.0" and
"newer version is available". I read that file for secrets before posting and never read what
it said.

Upgraded to 0.149.0 and re-ran everything: -s is still rejected by exec resume, the
four-phase result is unchanged, and resume --help still does not mention -c sandbox_mode.

Correction 2 — config.toml works, as you suspected

CODEX_HOME pointed 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_mode in config.toml does reach resumed sessions.

The isolated cause — it is trust_level, not the session policy

Your config.toml suggestion made me look at that file, where I found entries my own runs had
written: [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:

[1] exec -s read-only            → not created; config.toml: 0 lines (read-only writes no trust)
[2] resume, no flag              → not created
    ... then I append by hand:
    [projects."/tmp/tmp.Yq28..."]
    trust_level = "trusted"
[3] resume, no flag              → CREATED

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:

  1. On exec, an explicit -s read-only overrides directory trust — phase 1 above did not

write despite the directory being trusted.

  1. On exec resume, -s is 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-write run — outlives it, and resume offers no way to
override it the way exec does. Precedence appears to be config.toml sandbox_mode >
trust_level > default: with sandbox_mode = "read-only" set, the trusted directory did not
write.

What I would ask now

  1. Let exec resume accept -s/--sandbox, so the same override available on exec exists on

resume. This alone closes the gap.

  1. Document the precedence between trust_level, sandbox_mode and -s — I could not find it,

and I reached it by measurement.

  1. At minimum, mention -c sandbox_mode in resume --help, since it is the only working route

today (#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.

antoniodelia-hash · 5 days ago

@Hughhhhcoder - I checked your source reading against main before relying on it, and it holds:
ResumeArgs in codex-rs/exec/src/cli.rs carries only session_id, last, all, images
and prompt, with no sandbox field, and the single sandbox occurrence in that file sits
inside 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 directory
trust_level is part of that environment. On exec an explicit -s read-only overrides trust;
on resume that 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_mode bypasses the missing flag rather than restoring inheritance. Setting
sandbox_mode = "read-only" in config.toml suppressed 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_sandbox is
promoted to the subcommands — it shows up in resume --help — while sandbox selection is not.
So on exec resume today the option that removes the sandbox is available and the option that
sets 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 trusted
directory with an explicit config — I can run it and post the outcomes.