openai / openai/codex

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

Open
#40,149 5 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

bug CLI exec sandbox session
Dominant language
Rust
Stars
125k
Forks
19.4k
PR merge metrics
PR metrics pending

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the non-interactive sequence using codex exec with -s read-only, then codex exec resume and compare how -s/--sandbox is parsed and enforced. Trace the resume entry point and sandbox configuration flow; done means resume accepts the sandbox option and a resumed turn cannot write a file when read-only is selected.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
cli, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.