Desktop automations silently fall back to workspace-write sandbox regardless of app configuration
Summary
Desktop automations (scheduled/recurring tasks) start threads with workspace-write sandbox even when the application is configured for full access (danger-full-access). The sandbox policy only corrects to the intended value after a user manually enters the chat UI, at which point the desktop app injects the correct permissions via turn/start with sandbox_policy override. This makes unattended Git automations (pull, branch, commit, PR) fail with permission denied on .git/ paths, and forces manual intervention in workflows designed to be autonomous.
Root Cause (confirmed via source analysis)
The app-server protocol's thread/start RPC accepts an optional sandbox parameter in ThreadStartParams ([codex-rs/app-server-protocol/src/protocol/v2.rs:2481](<https://github.com/openai/codex/blob/main/codex-rs/app-server-protocol/src/protocol/v2.rs#L2481>)). When omitted, the app-server resolves the sandbox policy from the layered config (config.toml, CLI overrides, project trust) via derive_sandbox_policy() ([codex-rs/core/src/config/mod.rs:1747](<https://github.com/openai/codex/blob/main/codex-rs/core/src/config/mod.rs#L1747>)).
The desktop app stores its sandbox preference in .codex-global-state.json (e.g., agent-mode: full-access, skip-full-access-confirm: true), which is not part of the app-server's config resolution chain. The app-server has no mechanism to read this file.
Two code paths diverge:
<!-- linear:table-colwidths:266,266,266 -->
| Path | Behavior | Why |
| -- | -- | -- |
| Interactive chat (thread/start + turn/start) | Correct sandbox | Desktop app sends sandbox: "danger-full-access" in thread/start or overrides via turn/start with sandbox_policy → Op::OverrideTurnContext |
| Automation scheduler | Wrong sandbox (workspace-write) | Automation launcher sends thread/start WITHOUT the sandbox parameter → app-server falls back to config.toml default (typically workspace-write) |
Why it "fixes itself" after chat interaction:<br>When the user opens the chat UI, the desktop app sends turn/start with sandbox_policy: "danger-full-access", which triggers Op::OverrideTurnContext ([codex-rs/app-server/src/codex_message_processor.rs:6023](<https://github.com/openai/codex/blob/main/codex-rs/app-server/src/codex_message_processor.rs#L6023>)) to override the running thread's sandbox policy.
Impact
- Breaks unattended Git automations (permission denied on
.git/FETCH_HEAD,.git/refs/headslockfiles) - Causes false
Permission deniedfailures on workspace writes - Forces manual intervention in workflows designed to be autonomous
- Creates user confusion: "I set full access, why is it restricted?"
Evidence from local state
From .codex-global-state.json:
{
"agent-mode": "full-access",
"skip-full-access-confirm": true
}
From rollout JSONL (first turn context of automation):
{
"sandbox_policy": {"type": "workspace-write"},
"network_access": false
}
From rollout JSONL (after chat re-entry, same conversation):
{
"developer_instructions": "... danger-full-access ..."
}
From state_5.sqlite threads table — mixed sandbox policies for the same automation title:
- Some runs:
{"type":"danger-full-access"} - Other runs:
{"type":"workspace-write", "network_access": false}
Related Issues
This issue is part of a broader pattern of sandbox policy propagation failures across the Codex platform. The following issues describe related or overlapping symptoms:
Automations and background execution
<!-- linear:table-colwidths:200,200,200,200 -->
| Issue | Title | Status | Relationship |
| -- | -- | -- | -- |
| openai/codex#14590 | Desktop automations can start in workspace-write and switch to danger-full-access after chat interaction | OPEN | This issue's predecessor — same root cause |
| openai/codex#12919 | Scheduled automations cannot access GitHub API in default sandbox despite allowlisted gh api and loaded token | OPEN | Direct symptom of this bug — automation gets CODEX_SANDBOX_NETWORK_DISABLED=1 and GH_TOKEN=missing because sandbox policy is wrong |
| openai/codex#14741 | Automations fail to use MCP servers | OPEN | Direct symptom — MCP servers are sandbox-restricted in automations because the sandbox defaults to workspace-write |
| openai/codex#15161 | Thread-level sandbox in Codex App | OPEN | Feature request that would partially address this — per-thread sandbox override would let automations declare their own sandbox |
| [#10695](<https://github.com/openai/codex/issues/10695>) | Codex App cannot use Github Fix CI skill due to keychain and GH_TOKEN env being inaccessible in its sandbox | OPEN | Overlapping — desktop app sandbox restricts env/keychain access for skill execution |
Sandbox policy propagation failures
<!-- linear:table-colwidths:200,200,200,200 -->
| Issue | Title | Status | Relationship |
| -- | -- | -- | -- |
| openai/codex#15305 | Review-mode subagent ignores runtime sandbox override and falls back to config defaults | OPEN | Same pattern, different surface — review subagent inherits stale config instead of parent thread's effective runtime sandbox override |
| openai/codex#14068 | codex app-server tool commands run in read-only sandbox despite --dangerously-bypass-approvals-and-sandbox | OPEN | Same pattern, different surface — app-server's --dangerously-bypass flag doesn't propagate to tool child processes; strace confirms codex-linux-sandbox is invoked with {"type":"read-only"} |
| openai/codex#15309 | Approved escalated commands still inherit restricted network policy in interactive CLI sessions | OPEN | Same pattern, different surface — approval flow drops filesystem sandbox but preserves restricted NetworkSandboxPolicy, so approved commands still see CODEX_SANDBOX_NETWORK_DISABLED=1 |
| openai/codex#12996 | Codex.app injecting restricted network access | OPEN | Overlapping — desktop app injects restricted network policy that overrides config.toml network_access = true |
| [#10856](<https://github.com/openai/codex/issues/10856>) | codex app can't touch web, but codex cli can | OPEN | Overlapping — desktop app has different network/DNS behavior than CLI, likely due to sandbox policy differences |
Broader sandbox inconsistency issues
<!-- linear:table-colwidths:200,200,200,200 -->
| Issue | Title | Status | Relationship |
| -- | -- | -- | -- |
| openai/codex#5038 (closed) | VS Code extension ignores approval_policy="never" and randomly asks for approval | CLOSED | Historical — earlier manifestation of the same class of problem: client-side config not propagating to the agent runtime |
| openai/codex#5090 (closed) | Regression: sandbox/network enforcement breaks Codex autonomy | CLOSED | Historical — follow-up to openai/codex#5038, same pattern of sandbox/network enforcement ignoring configured permissions |
| openai/codex#9298 | Requires permissions escalation for every network request even when network_access = true | OPEN | Overlapping — network sandbox enforcement ignores configured policy |
| [#10099](<https://github.com/openai/codex/issues/10099>) | VS Code OpenAI codex keeps asking for permissions | OPEN | Overlapping — approval policy not persisting across sessions |
| openai/codex#14338 | Allow writable gitdir for current worktree in sandboxed workspace-write mode | OPEN | Tangential — workspace-write sandbox is too restrictive for Git operations, compounding this issue's impact |
| openai/codex#12716 | Allow list for commands instead of danger-full-access + deny list | OPEN | Feature request — would provide a middle ground between full-access and workspace-write |
Architecture Detail
How sandbox policy is resolved (current behavior)
thread/start (caller provides sandbox?)
│
├─ YES → ConfigOverrides.sandbox_mode = Some(DangerFullAccess)
│ └─ derive_sandbox_policy(sandbox_mode_override=Some(...))
│ └─ resolved = DangerFullAccess ✓
│
└─ NO → ConfigOverrides.sandbox_mode = None
└─ derive_sandbox_policy(sandbox_mode_override=None)
├─ check profile sandbox_mode → None (no profile set)
├─ check config.toml sandbox_mode → None (not set)
├─ check project trust → trusted? workspace-write
└─ default → ReadOnly or WorkspaceWrite ✗
How the interactive path works (correct)
Desktop App (interactive)
├─ Reads .codex-global-state.json → agent-mode = "full-access"
├─ Maps to SandboxMode::DangerFullAccess
├─ thread/start { sandbox: "danger-full-access" }
│ └─ ConfigOverrides.sandbox_mode = Some(DangerFullAccess) ✓
└─ turn/start { sandbox_policy: "danger-full-access" }
└─ Op::OverrideTurnContext ✓
How the automation path fails (broken)
Desktop App (automation scheduler)
├─ Reads .codex-global-state.json → agent-mode = "full-access"
├─ (does NOT pass sandbox to thread/start)
├─ thread/start { /* no sandbox field */ }
│ └─ ConfigOverrides.sandbox_mode = None ✗
│ └─ derive_sandbox_policy → defaults to workspace-write ✗
└─ Later: user opens chat UI
└─ turn/start { sandbox_policy: "danger-full-access" }
└─ Op::OverrideTurnContext ✓ (fixes it retroactively)
Proposed Fix
The protocol already supports the required behavior — ThreadStartParams.sandbox exists and accepts the full range of sandbox modes. No protocol changes are needed. The fix is in the desktop app (closed-source):
Option A: Always pass sandbox in thread/start (recommended)
The automation launcher should read the app's configured sandbox preference and pass it in every thread/start call:
{ "method": "thread/start", "params": {
"cwd": "/path/to/workspace",
"sandbox": "danger-full-access",
...
} }
This is the simplest fix and guarantees every thread gets the intended policy regardless of config.toml.
Option B: Persist sandbox preference to config.toml
When the user changes the sandbox mode in the desktop app UI, write sandbox_mode to config.toml via the existing config/batch/write API:
{ "method": "config/batch/write", "params": {
"edits": [{ "keyPath": "sandbox_mode", "value": "danger-full-access" }],
"reloadUserConfig": true
} }
This makes the preference visible to all thread-creation paths automatically, including those that don't supply an explicit sandbox override.
Option C: Per-automation sandbox configuration
Add a sandbox mode selector to the automation setup UI (related: openai/codex#15161). When creating/editing an automation, the user selects the desired sandbox mode. The launcher stores this value and passes it in thread/start. This gives the most flexibility but requires UI changes.
Documentation Fix (submitted)
A documentation PR has been submitted that clarifies the ThreadStartParams.sandbox fallback behavior and documents the two strategies above for clients that manage their own sandbox preferences. The PR adds:
- Doc comment to
ThreadStartParams.sandboxexplaining the fallback toconfig.toml - "Sandbox defaults for clients with external preferences" section in the app-server README
Acceptance Criteria
- An automation configured to run with full access starts its thread with
danger-full-accesssandbox from the first turn, without requiring user interaction - The thread's rollout JSONL shows
sandbox_policy: {"type": "danger-full-access"}in the first turn context, notworkspace-write - Git operations (
git pull, branch creation, commit, PR) succeed in the automation's first turn without permission errors - The behavior is consistent: the automation sandbox policy does not change between the "background" phase and the "chat UI" phase
19 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
this UI-layer state vs scheduler path desync is a classic desktop agent pattern. hit it on a mac agent project earlier this year where Accessibility checks passed interactively but failed silently from a launchd run because the TCC prompt context never fired. root cause was the same shape, privileged config lives in a process the scheduler never hydrates from. what worked for us was passing the full policy envelope on thread spawn rather than falling back to config.toml defaults. the self-heal after chat re-entry proves the override path works, it's just not wired into the unattended launch.
Additional sanitized reproduction from today.
Environment:
0.125.0-alpha.3codex-cli 0.30.0gh 2.90.0Automation configuration:
Observed behavior:
execution_environment = "local".sandbox_mode = read-onlyNetwork access is restrictedApproval policy is currently nevergh auth statusreported the stored token as invalid.gh issue list .../gh api ...failed witherror connecting to api.github.com.curl https://api.github.com/...failed withCould not resolve host: api.github.com.operation not permitted.gh auth statussucceeded using the same account/keyring.repoandworkflowscopes.ghcould access the target private repository.Why this matters:
This is not just a stale GitHub token. The same host/keyring works outside the automation run. The automation runtime appears to ignore or fail to propagate the
localexecution environment into the first turn's effective sandbox/network policy. Because the automation run hasapproval_policy = never, the agent cannot recover by requesting escalation.Expected behavior:
For a cron automation configured with
execution_environment = "local", the first turn should start with the effective local/full-access permissions needed for local keyring access, network access, and writes to the configured automation memory path, or the UI/config should expose a separate explicit automation sandbox/network selector iflocalis not intended to mean that.Additional sanitized reproduction from a Codex Desktop scheduled automation on macOS.
Environment:
execution_environment = "local"and a workspace cwdObserved behavior:
twitter status --yamlreports authenticated successfully.env -i HOME=/Users/<user> USER=<user> PATH=... twitter status --yaml.not_authenticated.PermissionError/Operation not permitted.Why this matters:
This is not a missing credential, stale login, or PATH/env problem. The credential is valid and readable from the same host/user in an interactive session, even with a minimal environment. The scheduled automation runtime appears to run in a different effective permission context that cannot reliably access macOS Keychain/browser-cookie backed auth, despite being configured as local execution.
Current workaround:
The automation now has to hard-stop on a live auth preflight before fetching or writing any state, otherwise it can overwrite valid timeline data with an empty/partial result. That prevents data corruption but means the scheduled automation cannot reliably run unattended.
Expected behavior:
For scheduled automations configured with local/full-access execution, the first turn should have the same effective local permissions needed for Keychain/browser-cookie backed CLI auth and configured workspace/state writes, or the UI should expose a separate explicit automation permission model and make clear that unattended runs cannot access those resources.
Can confirm again.
same issue
same issue
same issue
Adding another Windows repro from Codex Desktop App 26.430.10722 observed on 2026-05-07.
Environment:
config.tomlcontainssandbox_mode = "danger-full-access"[windows] sandbox = "elevated"on-requestObserved behavior:
PermissionError: [WinError 10013].PermissionError: [WinError 10013].Expected behavior:
danger-full-access, the initial tool and automation execution should not inherit a network-disabled/socket-restricted sandbox.This looks related to #12996 and #13373, but on Windows the directly observable failure is
WinError 10013rather than only DNS/GitHub API failures.This is also a big problem for me and makes current automations almost useless. At least I need abilty for git operations (like git pull) and access to at least a select set of websites.
Experiencing this behaviour in Windows for Automates as well. For me, it also causes the permissions dropdown in the Codex desktop app as well as plan mode to be greyed out completely.
Same issue - this makes automations pointless for me
I can confirm this issue affects Codex Desktop automations that need outbound SSH.
In my case, the same SSH command worked in an interactive Codex thread, but failed in a scheduled desktop automation with:
Local
state_5.sqliteshowed the automation thread was started with:while the interactive thread had a less restrictive effective policy.
A practical workaround that fixed it for me was adding this to
~/.codex/config.tomland restarting Codex Desktop:After restart, the same scheduled automation successfully connected over SSH while keeping the filesystem sandbox as
workspace-write.So the issue is not limited to Git operations. It also affects automations that need SSH/network access. A good product-level fix would be for the automation launcher to pass the intended sandbox/network policy at
thread/start, or persist the Desktop UI sandbox/network preference intoconfig.tomlso background automations resolve the same policy as interactive threads.We are seeing the same problem on macOS with Codex Desktop scheduled automations, and it makes automations very hard to use for anything that needs local state, SSH, or durable memory writes.
Relevant config in
~/.codex/config.toml:The automation itself is configured as local:
But the effective first-turn context does not match the config. From our local rollout JSONL:
An earlier scheduled run of the same automation also started as:
Observed failures in scheduled automation:
Operation not permitted: '/Users/<user>/.tlap-ai-sysadmin/runs/.audit.lock'mkdir: /private/tmp/...: Operation not permittedssh: connect to host <host> port 22: Operation not permittedoperation not permitted: /Users/<user>/.codex/automations/<automation-id>/memory.mdThe exact same prompt run manually in a new Codex chat succeeds, including the SSH-based audit and local report writes. So this looks like the automation launcher is not propagating the configured full-access/local policy into the actual first turn.
Same issue
Same issue here.
Environment:
approval_policy = "never"andsandbox_mode = "danger-full-access";codex doctorreports unrestricted filesystem, network enabled, approval Never for normal interactive sessions.Observed in a scheduled local automation:
brew upgradefailed before upgrading because it could not write Homebrew cache files (Operation not permitted).npm update -g -dcould not reachregistry.npmjs.orgduring the automation run (ENOTFOUND/ restricted network behavior), while the current interactive environment has network enabled.This matches the described mismatch where interactive sessions honor full access, but unattended Desktop automations do not consistently inherit the same sandbox/network policy.
Additional evidence from a projectless scheduled automation on macOS.
Environment:
Automation config:
Observed on 2026-07-03:
019f26ff-669b-7151-a54a-c67acd13d88dstarted with:No config change happened between those two contexts.
~/Dev/typebot.iostarted correctly with:Impact of the first-turn projectless read-only sandbox:
touch work/write-testand shell here-doc temp creation both returnedOperation not permitted).STRAVA_CLIENT_SECRET,STRAVA_REFRESH_TOKEN, Dougs, Gmail), so external API phases could not run.Source/doc cross-check:
ThreadStartParams.sandbox.@openai/codex@0.142.5,SandboxModedefaults toread-only.ConfigToml::derive_permission_profileonly falls back toworkspace-writewhen the active project has a trusted or untrusted project decision; otherwise it uses the default sandbox mode.This looks like the desktop automation scheduler/projectless path is starting the thread without passing the configured
sandbox: "danger-full-access"and without anchoring the run to a trusted cwd, so app-server falls back to theread-onlydefault for the first automation turn. Once the thread is opened in the UI, the correct full-access context is injected retroactively.same issue
Additional reproduction: cross-thread follow-ups also lose Full Access
This propagation bug is not limited to scheduled automations. I can reproduce the same policy downgrade when the Codex Desktop App sends a follow-up to an existing task through
send_message_to_thread.Environment
26.707.3748.0Microsoft Windows NT 10.0.22631.0 x64Reproduction
``
json
``{
"approval_policy": "never",
"sandbox_policy": {"type": "danger-full-access"},
"permission_profile": {"type": "disabled"}
}
send_message_to_threadtool to send a follow-up to an existing long-running task.``
json
``{
"approval_policy": "on-request",
"sandbox_policy": {
"type": "workspace-write",
"network_access": false
},
"permission_profile": {
"type": "managed",
"network": "restricted"
}
}
continuewith Full access still selected.danger-full-access+approval_policy: never+ disabled permission profile, with no config change.Observed scope
<codex_delegation>follow-up.exec_commandcalls withsandbox_permissions: "require_escalated"after these restricted turns.``
text
``CreateProcessWithLogonW failed: 1385
The agent then retries with escalation, causing repeated permission prompts even though the user selected Full access.
Expected behavior
A cross-thread follow-up should use either:
but it should not silently fall back to
workspace-write/on-request.At minimum,
send_message_to_threadshould expose explicit sandbox/approval overrides. This is also consistent with the requested API shape in #14923.This appears to be the same underlying client-side policy propagation class described in this issue, but through the cross-thread follow-up launcher instead of the automation scheduler. All task IDs, local paths, and account details are intentionally redacted.