Windows Desktop: new threads/handoffs persist as on-request/workspaceWrite instead of inheriting visible Full Access state

Open 💬 12 comments Opened Jun 2, 2026 by andrewkangkr
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Summary

On Windows Codex Desktop, newly created threads can persist as approvalPolicy = on-request / sandboxPolicy.type = workspaceWrite even when the user workflow appears to be operating in a Full Access-like mode.

This is reproducible with:

  • ordinary fresh test threads
  • handoff-created threads

and does not require Goal auto-continuation.

Environment

  • Codex Desktop app: OpenAI.Codex_26.527.7698.0_x64__2p2nqsd0c76g0
  • Platform: Windows

Expected

If the user has switched into a Full Access-like mode in the app, newly created threads should inherit that mode consistently, or the UI should clearly indicate that new threads will still be created with approval prompts.

Actual

Newly created threads are persisted as:

  • approvalPolicy = on-request
  • approvalsReviewer = guardian_subagent
  • sandboxPolicy.type = workspaceWrite

This then causes approval prompts to originate from those new threads.

Repro

  1. Open Codex Desktop on Windows.
  2. Work in a thread until the UI/workflow suggests Full Access-like behavior is active.
  3. Create a new thread from that session, or create a handoff into a new thread.
  4. Let the new thread do almost nothing, e.g. only answer once with a short reply.
  5. Inspect persisted thread state in C:\Users\강인철\.codex\.codex-global-state.json.
  6. Observe that the new thread is stored as on-request / guardian_subagent / workspaceWrite.

Concrete repro evidence

Host-level persisted mode in app state:

  • agent-mode-by-host-id.local = guardian-approvals
  • preferred-non-full-access-agent-mode-by-host-id.local = guardian-approvals

Fresh / recent threads created during reproduction:

  • current thread 019e866c-0a1c-7601-a60c-686b686bcc5c -> on-request / guardian_subagent / workspaceWrite
  • test thread 019e8764-2aa9-73d2-9b3d-87ecb202af5a -> on-request / guardian_subagent / workspaceWrite
  • test thread 019e8765-f908-7410-b44d-443df7934c73 -> on-request / guardian_subagent / workspaceWrite
  • test thread 019e877d-fb12-7683-8033-3f909b8ca714 -> on-request / guardian_subagent / workspaceWrite
  • handoff-derived thread 019e8689-b909-7ed1-a141-d0b3323a21cf -> on-request / guardian_subagent / workspaceWrite

Additional pattern:

  • among currently stored thread states on this machine, only 1 thread had approvalPolicy = never
  • 60 threads had approvalPolicy = on-request

Why this seems like a bug

This does not look like a one-off stuck thread.

It looks like one of:

  • thread creation is not inheriting the currently visible Full Access selection
  • host-level permission mode is not being committed correctly before thread creation
  • handoff/new-thread creation is using a different policy source than the visible UI state
  • the app is exposing a misleading Full Access state to the user while still creating threads in guardian-approvals mode

Related

This may be related to, but broader than, the behavior discussed in #24300.

I also added Windows reproduction details there in a comment:

View original on GitHub ↗

12 Comments

github-actions[bot] contributor · 2 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #25590
  • #24739

Powered by Codex Action

andrewkangkr · 2 months ago

Small follow-up after checking the related discussion in #24300:

  • an OpenAI collaborator there said the currently known issue can be worked around by sending a dummy prompt after changing permissions and before creating a Goal
  • however, my Windows Desktop repro appears broader than that specific Goal-related case

In my reproduction:

  • this happens even without Goal auto-continuation
  • it also happens for newly created test threads that already received a user prompt and then stopped

Concrete examples from my local persisted thread state:

  • 019e8764-2aa9-73d2-9b3d-87ecb202af5a -> on-request / guardian_subagent / workspaceWrite
  • 019e8765-f908-7410-b44d-443df7934c73 -> on-request / guardian_subagent / workspaceWrite
  • 019e877d-fb12-7683-8033-3f909b8ca714 -> on-request / guardian_subagent / workspaceWrite

Those were fresh test threads that only received a short user prompt and replied once.

So if this is the same root cause, it seems to affect thread creation / policy inheritance more generally on Windows Desktop, not only the "change permission -> create Goal" path.

andrewkangkr · 2 months ago

Additional Windows Desktop follow-up from June 3, 2026:

I have a clearer split now between auto-review vs thread policy.

What I am seeing is not just "auto-review failed to approve something".
Instead, the underlying thread policy itself appears to revert back to restrictive settings after thread execution starts.

New observation

After switching the app-level host mode to Full Access, I could verify the persisted host value became:

  • agent-mode-by-host-id.local = full-access

I then created fresh threads and briefly saw cases that looked consistent with:

  • approvalPolicy = never
  • sandboxPolicy.type = dangerFullAccess

However, after the thread actually started running / continuing, persisted state later showed the same thread as:

  • approvalPolicy = on-request
  • sandboxPolicy.type = workspaceWrite

Why this matters

This suggests the problem may be broader than "new thread creation inherits the wrong initial settings".
It may also involve policy reversion during thread start / resume / goal-setting / delegated execution.

So the bug shape now looks more like:

  1. visible/app-level Full Access can be selected successfully
  2. a new thread may initially appear consistent with Full Access
  3. once the thread actually begins execution, persisted thread policy can be rewritten back to on-request / workspaceWrite
  4. approval popups then come from that downgraded persisted policy

Important distinction: auto-review vs approval policy

At least in my local investigation, auto-review seems to be a separate concept from the popup itself:

  • auto-review / reviewer affects who reviews an approval request
  • approvalPolicy determines whether the thread is effectively never vs on-request

So the user-facing symptom can look like "auto-review is broken", while the deeper issue is actually that the thread got persisted back to on-request.

Scope note

This still does not look Goal-only to me.
I have seen related behavior in:

  • ordinary test threads
  • handoff/delegated threads
  • a Goal-adjacent AKN thread where the thread log later said it set an active goal for itself

So Goal may be one trigger path, but not the whole story.

If useful, I can provide another tightly scoped repro sequence specifically for:

  • create thread under Full Access
  • verify persisted never / dangerFullAccess
  • let thread actually start/continue
  • observe persisted reversion to on-request / workspaceWrite
andrewkangkr · 2 months ago

Additional Windows Desktop / mobile-resume detail that seems worth tracking separately even if the original approvalPolicy drift is fixed.

New behavior observed on June 4, 2026 (KST):

  • I had a parked handoff thread whose explicit instruction was "do not set a goal or proceed; just park the handoff".
  • The parked thread was projectless / handoff-shell style, with cwd like:

C:\Users\<USER>\Documents\Codex\2026-06-03\specialization-layer-handoff-parked

  • The canonical source-of-truth repo for the work was elsewhere:

C:\Users\<USER>\Documents\Codex\2026-05-24\hermes-project-os-expert-agent\expert-repository-poc

After resuming from mobile and sending short messages like:

  • "남은 slice 2개인거지?"
  • "frontier는 어떤 상태?"
  • "1순위부터 해보자"

the parked thread stopped behaving like a parked handoff and resumed actual work. It started trying to read/edit files in the canonical repo outside the current workspace and produced approval prompts such as:

  • "allow reading and then editing the canonical expert repo outside the current workspace..."
  • reads of tools/validate.py
  • reads of tools/test_validate_specialization.py
  • planned patching of latest.md

In other words, the trigger was not a broad new user task from scratch; it was a few short follow-up messages inside a parked handoff thread. The app seems to treat that parked thread as a live execution surface even though its workspace is only a handoff shell, not the real repo.

Separately, one of these parked threads later hit a Windows stale-path resume error even though the JSONL file still existed on disk. The error compared:

  • requested: C:\Users\<USER>\.codex\sessions\...jsonl
  • active: \\?\C:\Users\<USER>\.codex\sessions\...jsonl

Those are the same file, so this looks like another Windows path-normalization bug on top of the workspace mismatch. (Related in spirit to #24178.)

My current working theory:

  1. Parked handoff threads are not strongly isolated from real execution after mobile follow-up input.
  2. Once they resume, Codex continues from the parked thread's current workspace instead of forcing a proper canonical-repo work thread.
  3. That leads to misleading outside-workspace approval prompts.
  4. On Windows, some of those same threads can later fail resume due to raw-string path comparison (C:\... vs \\?\C:\...).

Practical expectation:

  • If a thread is meant to be parked/handoff-only, short follow-up questions should not silently turn it into a real editing thread against a different canonical repo.
  • If work does resume, Codex should either force a proper workspace transition or make the mismatch explicit before attempting outside-workspace reads/edits.
bakemocho · 2 months ago

I have an additional macOS reproduction that looks like the same permission
profile reversion class, but through the Goal quota-reset resume path rather
than Windows new-thread creation.

Observed sequence:

  • A Goal-driven session stopped because of quota limit.
  • Another Goal-driven session showed the same kind of stop.
  • I quit Codex Desktop, rebooted macOS, reopened Codex, used the quota reset

feature, and resumed the existing Goal session.

  • Before pressing resume, the Japanese UI showed ゴールの使用回数制限あり.

I did not confirm what the English UI label says.

  • Toggling the app UI/permission button to Full Access, and trying related UI

permission changes, did not recover this session.

  • After resume, the app/global state still appeared to be full access, but the

resumed thread asked for approvals.

Sanitized local state:

  • global app state included agent-mode = full-access and

agent-mode-by-host-id.local = full-access

  • the affected thread-specific entry still had

activePermissionProfile.id = :workspace

  • the same entry had approvalPolicy = on-request
  • the same entry had sandboxPolicy.type = workspaceWrite
  • the resumed runtime still reported a seatbelt sandbox with network disabled

This seems to add a narrower trigger to the pattern already discussed here:

  1. the UI/global mode can be full-access,
  2. an existing Goal session is resumed after quota-limit recovery,
  3. the resumed thread keeps or receives a stale restricted thread-specific

permission profile,

  1. later UI permission changes do not necessarily repair that thread-specific

state,

  1. the running turn then behaves as workspace-write/on-request.

Expected behavior: after quota reset, resuming an existing Goal session should
either reconcile the thread-specific permission profile with the current
full-access app mode, or make the per-thread restricted override visible before
the resume starts.

I am intentionally not including full local state, thread ids, or prompt
history because those contain sensitive data.

bakemocho · 2 months ago

Small macOS follow-up to my previous comment.

I have now seen a similar permission mismatch in existing sessions that were not using Goal Mode. So the paused Goal / quota-reset path does not look like a required trigger. It may still be one trigger or amplifier, but the broader class appears to be existing-session resume / per-thread permission state getting out of sync with the visible app-level Full Access mode.

Additional recovery observation:

After restarting Codex Desktop, the same affected session now appears consistent across the three surfaces I checked:

  • runtime no longer exposes CODEX_SANDBOX=seatbelt or CODEX_SANDBOX_NETWORK_DISABLED=1
  • .codex-global-state.json shows app mode full-access, and this thread has :danger-full-access / never / dangerFullAccess
  • state_5.sqlite shows this thread as approval_mode=never and sandbox_policy={"type":"disabled"}

So the current evidence from my macOS side is:

  1. UI/global Full Access can diverge from a resumed existing thread's effective policy.
  2. This is not necessarily Goal-only.
  3. Restarting the Desktop app can reconcile the affected thread back to the expected Full Access state, at least in this case.

I am still omitting local thread ids and prompt history because they contain private data.

bakemocho · 2 months ago

Small follow-up with a local workaround observation after this reproduced again on macOS in a retained existing session.

In the new case, the visible app/host mode was Full Access, but the active thread still launched with a restricted effective policy. The useful split was checking two persisted state surfaces for the affected thread:

  • the thread row in state_5.sqlite
  • the thread entry under heartbeat-thread-permissions-by-id in .codex-global-state.json

The emergency workaround that recovered the persisted state for me was to back up both files, then align both surfaces for the affected thread:

  • state_5.sqlite: set the thread to approval_mode = never and sandbox_policy = {"type":"disabled"}
  • .codex-global-state.json: set the thread entry to activePermissionProfile.id = :danger-full-access, approvalPolicy = never, and sandboxPolicy.type = dangerFullAccess

A reload/resume of the affected thread is still required after the state repair. The already-running turn does not adopt the new sandbox profile just because the files were changed.

This is only an unsupported local emergency workaround, not a root-cause claim or a suggested product fix. I am not including local thread ids, absolute paths, prompt history, or private state contents.

sunjiajunsunjiajun · 1 month ago

I am seeing a related inheritance problem with delegated/subagent work.

After granting Full Access, the parent agent appears to acknowledge that the permission state has been updated. But when the parent dispatches a subagent, the subagent does not behave as if it inherited that updated Full Access state. The child still appears to operate under a stale or restricted permission context, which causes delegated work to fail or hit permission boundaries unexpectedly.

This is confusing because the visible state suggests the current workflow is allowed to proceed with Full Access, but newly created child work does not consistently inherit that state.

korsun009 · 19 days ago

I found a concrete downgrade path on Windows Codex Desktop 26.803.5235.0 (embedded app-server 0.130.0-alpha.5).

Persisted state from an older installation contained:

"composer-permission-mode-visibility": true

The current Desktop code expects an object:

{
  "guardian-approvals": true,
  "full-access": true
}

Because the persisted boolean is accepted without schema migration, both mode properties are effectively missing. Full Access briefly appears during startup, then is filtered from the visible agent modes. The reconciliation effect switches the active mode back to auto and rewrites the current thread permissions to on-request / workspaceWrite.

The Codex configuration itself was valid:

sandbox_mode = "danger-full-access"
approval_policy = "never"

configRequirements/read also returned requirements: null.

Replacing the legacy boolean with the current object representation fixed the problem, and Full Access now persists across application restarts.

Suggested fix: migrate legacy boolean values to the current object schema, or validate the persisted value type and fall back to the current default object before filtering the available permission modes.

4Liberty · 19 days ago

I reproduced a distinct cross-host delegated-creation downgrade in the current signed macOS Desktop build.

Source client:

  • Codex Desktop 26.803.41515, bundle 6321
  • signed app.asar SHA-256 5f6e773aafd542d3cf09e10b5dca6cabd301d0a155f4b8ce870e3915fc3da25e
  • source turn: approval_policy=never, sandbox_policy=danger-full-access, unrestricted permission profile
  • saved destination host selection: full-access

The source called the official create_thread tool for a remote HOMELAND Git project without a permission override. The receiver's first turn was nevertheless persisted as approval_policy=on-request, sandbox_policy.type=workspace-write, managed/restricted filesystem.

Read-only inspection of the signed renderer shows the conversion is explicit in the cross-host delegated path. In the minified build, C5o(...) reads agent-mode-by-host-id, but when threadSource === "subagent" it maps a selected full-access mode to auto unless a same-host source permission object can be reused. A cross-host source cannot supply that reusable object, so b5o(...) sends agentMode: "auto" with shouldSendPermissionOverrides: true. This occurs before the destination's healthy default_permissions = ":danger-full-access" can apply.

This also explains why repairing the destination config and the source's per-host selection does not change the result, while a locally created destination task starts correctly with full access.

Expected: cross-host create_thread should either preserve the explicitly selected destination-host permission mode, omit the override so destination defaults resolve it, or expose an explicit permission argument/consent path. It should not silently convert the owner's full-access selection to auto.

I have installed a local exact-build fail-closed guard so this signed build no longer creates high-authority cross-host tasks. I can provide sanitized rollout excerpts or the relevant minified function body if useful.

schmidtc63 · 15 days ago

Additional deterministic callback/worktree reproduction and narrowed root cause

I reproduced this on Windows with Codex Desktop Store package 26.803.10989.0 (embedded app build 26.803.81509). This is a controlled same-parent/same-create-payload comparison:

  • A worktree child created from the parent task's foreground turn started with approvalPolicy=never, sandboxPolicy=dangerFullAccess, and the built-in :danger-full-access profile.
  • The next worktree child was created from the same parent after a background delegated callback, using the same create_thread target shape. The parent callback turn itself still had effective never + dangerFullAccess, but its active permission profile ID was null. The child started as on-request + workspaceWrite + networkAccess:false.
  • Opening that same failed child in the primary window and sending a foreground retry immediately changed its next turn to never + dangerFullAccess.

The renderer's cross-CWD permission-selection step appears to preserve a source selection when either:

  1. source and target CWD match, or
  2. the selection has a built-in profile ID beginning with :.

It rejects the callback representation when source and target CWD differ and the effective mode is Full Access but the profile ID is null. The delegated-child fallback then silently resolves to auto, which produces the observed managed/workspace-write child.

For the observed case, the narrow source correction is conceptually:

if (source.sourceCwd === targetCwd || source.agentMode === "full-access") {
  return source;
}

Full Access is not CWD-bound. The later worktree-finalization path can rebuild canonical never + dangerFullAccess for the new worktree roots.

For the durable fix, please also fail closed when a delegated create_thread has no recoverable effective source permission snapshot. It should not silently fall back to auto. A safe resolver can recover fieldwise from the source task's latest thread settings, latest turn parameters, and current permissions, but should omit host defaults for delegated creation.

Suggested focused regression matrix:

  • cross-CWD Full Access with permissionProfileId=null is inherited;
  • cross-CWD :danger-full-access continues to be inherited;
  • same-CWD existing behavior remains unchanged;
  • cross-CWD custom and guardian profiles are not elevated;
  • missing/unclassifiable delegated source permissions fail closed rather than producing auto;
  • direct foreground and background delegated callback creation produce the same child approval, sandbox, and network settings.

User impact: unattended controller workflows stop and require a human foreground retry even though the parent task had Full Access throughout. A follow-up sent by the parent cannot repair the child because send_message_to_thread carries no permission fields.

albert-zen · 4 days ago

Deterministic macOS Heartbeat trigger and narrowed root cause

I reproduced a deterministic instance of this permission-inheritance bug in the current signed macOS Codex Desktop build.

Environment

  • Codex Desktop: 26.818.41705 (bundle 6971)
  • Embedded app-server observed in the affected session: 0.149.0-alpha.4.1
  • Platform: Darwin 25.5.0 arm64
  • Source thread permission before the trigger: built-in :danger-full-access, approvalPolicy=never, sandboxPolicy=dangerFullAccess

Reproduction

  1. Select Full Access on a long-running parent thread.
  2. Verify that the source turn contains the built-in profile identity :danger-full-access.
  3. Let a scheduled Heartbeat wake/resume that same thread.
  4. The heartbeat turn still has effective approvalPolicy=never and sandboxPolicy=dangerFullAccess, but its activePermissionProfile is now null.
  5. From that heartbeat-resumed parent, call the official create_thread tool for a project/worktree whose CWD differs from the parent CWD.
  6. The new delegated thread starts as approvalPolicy=on-request, sandboxPolicy=workspaceWrite, with a managed/restricted filesystem.
  7. Retrying delegated creation without touching the permission selector reproduces the downgrade. Toggling the parent permission selector away from Full Access and back to Full Access restores :danger-full-access, after which cross-CWD creation inherits Full Access again.

The app-level host selection remains full-access throughout. The lost value is specifically the active profile identity on the heartbeat turn.

Narrowed renderer path

Static inspection of the signed packaged renderer suggests the following chain. Minified identifiers are included only as clues for this exact build:

  1. Heartbeat permission resolution (R2l / B2l) accepts a latest permission object with activePermissionProfile: null as authoritative.
  2. Because that object is otherwise complete, it takes precedence over the persisted heartbeat-thread-permissions-by-id fallback, even when the fallback still contains :danger-full-access.
  3. Delegated creation (roi) therefore derives Full Access with permissionProfileId=null.
  4. Cross-CWD inheritance (aoi) preserves built-in profiles only when the ID starts with :; it rejects this identity-less Full Access selection.
  5. The delegated fallback (ioi) deliberately maps host-level full-access to auto, yielding on-request/workspaceWrite.

Conceptually, the heartbeat merge currently distinguishes only between an absent field and a present field, but does not treat a null profile identity as incomplete:

latest heartbeat permissions (profile = null)
  > persisted heartbeat permissions (profile = :danger-full-access)
  > host selection

Expected behavior

A Heartbeat should not erase the selected built-in permission profile identity. After a Full Access parent is resumed by Heartbeat, delegated same-host project/worktree creation should inherit :danger-full-access, just as it does from a foreground parent turn.

A focused regression test would cover:

Full Access parent
  -> Heartbeat resume
  -> cross-CWD delegated create_thread
  -> child remains never + dangerFullAccess

Custom/CWD-bound profiles should retain their existing non-inheritance behavior; this report is about the built-in Full Access profile losing its identity before the existing cross-CWD check.

Current workaround

I found no supported Heartbeat, automation, or create_thread parameter that pins a permission profile. Prompt instructions cannot repair the turn context. The only reliable user-level workaround found so far is to reselect Full Access on the parent after a Heartbeat and before creating delegated cross-CWD work. Retrying creation while the profile remains null does not help.

This report was prepared and submitted by the GPT-5.6 Sol agent running in the Codex Desktop harness, on behalf of the authenticated GitHub account @albert-zen. Local paths, thread IDs, and prompt history were intentionally omitted.