[Windows app] Full Access -> Custom leaves stale approvals_reviewer=user and bypasses Auto-review

Open 💬 1 comment Opened Aug 16, 2026 by squarepots

What version of the Codex App are you using (From “About Codex” dialog)?

Version 26.810.52044

The same state-transition pattern also reproduced on this same computer in a separate task on July 19, 2026, while running the then-current Codex Desktop version.

What subscription do you have?

Not included in this public report; the issue appears to be local Desktop permission-state handling rather than subscription-specific behavior.

What platform is your computer?

Microsoft Windows NT 10.0.26200.0 x64

What issue are you seeing?

Switching a running Codex Desktop task from Custom (config.toml) to Full Access, then back to Custom, can restore the sandbox and approval policy but leave the approval reviewer from Full Access.

My user-level config is:

approval_policy = "on-request"
approvals_reviewer = "auto_review"
sandbox_mode = "workspace-write"

The expected Custom state is therefore:

approval_policy = on-request
approvals_reviewer = auto_review
sandbox_policy = workspace-write

However, the affected task transitions as follows:

1. Custom
   approval_policy = on-request
   approvals_reviewer = auto_review
   sandbox_policy = workspace-write

2. Switch to Full Access
   approval_policy = never
   approvals_reviewer = user
   sandbox_policy = danger-full-access

3. Switch back to Custom
   approval_policy = on-request
   approvals_reviewer = user        <-- stale / incorrect
   sandbox_policy = workspace-write

At step 3, approval requests are routed directly to the human UI. Auto-review never receives them, even though config.toml still says approvals_reviewer = "auto_review".

This produces the user-visible symptom of manual approval dialogs appearing for a task that is expected to be using Auto-review.

A fresh task at the same time correctly uses:

approval_policy = on-request
approvals_reviewer = auto_review
sandbox_policy = workspace-write

That rules out a malformed global config and points to stale per-thread/task permission state.

This has now reproduced in two separate tasks at different times. The earlier reproduction on July 19 showed the same transition:

on-request + auto_review
-> Full Access: never + user
-> back to managed/Custom: on-request + user

The later reproduction showed the same reviewer leak again after returning from Full Access.

What steps can reproduce the bug?

  1. On Windows Codex Desktop, configure:

``toml
approval_policy = "on-request"
approvals_reviewer = "auto_review"
sandbox_mode = "workspace-write"
``

  1. Start a fresh task in Custom (config.toml).
  2. Verify the effective task settings are:

``text
approval_policy = on-request
approvals_reviewer = auto_review
sandbox_policy = workspace-write
``

  1. Switch the same running task to Full Access.
  2. Verify the effective task settings become approximately:

``text
approval_policy = never
approvals_reviewer = user
sandbox_policy = danger-full-access
``

  1. Switch the same task back to Custom (config.toml).
  2. Trigger any operation that requires an escalation/approval under the managed sandbox.
  3. Inspect the effective task settings and approval routing.

Observed:

approval_policy = on-request
approvals_reviewer = user
sandbox_policy = workspace-write

The approval is shown to the user manually.

Expected:

approval_policy = on-request
approvals_reviewer = auto_review
sandbox_policy = workspace-write

The request should be routed to the Auto-review reviewer agent.

What is the expected behavior?

Switching permission modes should replace the complete effective permission context atomically.

When switching Full Access -> Custom (config.toml), Codex Desktop should restore all relevant fields from the configured Custom state, including approvals_reviewer.

A field from Full Access (approvals_reviewer = user) must not remain attached to the task after the sandbox and approval policy have already returned to the Custom values.

Additional information

This appears related to #32338, which reports broader permission-profile desynchronization and field mixing on Windows, but this report is intentionally narrower: it describes a repeatable Full Access -> Custom reviewer-state leak with a concrete three-state transition and a specific user-visible consequence: Auto-review is bypassed and manual approvals appear.

It also differs from reports where Auto-review rejects or fails an approval. In this case Auto-review is not involved at all: the task's effective approvals_reviewer has changed to user.

Manually selecting Approve for me restores approvals_reviewer = auto_review, and a new task also reads the global config correctly. This further suggests the bug is in per-thread/task permission-state restoration or field-wise merging during the Full Access -> Custom transition, rather than config parsing.

View original on GitHub ↗

1 Comment

jdcodes1 · 11 days ago

The "stale per-thread permission state" hypothesis is verifiable in this repo's shared app-server/TUI code, and the mechanism explains all three of your observations (reviewer leaks on the round-trip, fresh tasks are correct, and the leak survives restarts of the same task). Traced on main @ 1f41cc5d92.

1. Thread-settings overrides have no way to say "revert to config" — None means "keep"

When a client switches a running thread's permissions, it goes through thread/settings/updatebuild_thread_settings_overrides:

https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/app-server/src/request_processors/turn_processor.rs#L685-L825

Every field is an Option, and an absent field means "leave the thread's current value alone". Switching to Full Access explicitly sets approvals_reviewer = user on the thread. Switching back to a config-derived Custom profile sends the permissions selection — and crucially, when permissions is applied, the server calls load_for_cwd (which loads your full config, including approvals_reviewer = "auto_review") but extracts only the permission-profile fields from it (permission_profile, active_permission_profile, profile_workspace_roots); the loaded config's reviewer is discarded, and approvals_reviewer in the resulting overrides comes solely from the request params. So unless the client re-sends the reviewer explicitly, the thread keeps user from the Full Access step, while sandbox/policy get restored. There is currently no tri-state to express "clear this override and fall back to config".

2. The TUI exhibits the same leak, which localizes the bug to this shared layer

Selecting a custom permission profile in the TUI builds the selection with approval_policy: None, approvals_reviewer: None:

https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/tui/src/chatwidget/permissions_menu.rs#L188-L201

and apply_permission_profile_selection only touches the reviewer if let Some(reviewer) (config_persistence.rs#L179, #L203) — so after a Full Access round-trip, both the in-memory config and the OverrideTurnContext sent to the thread retain user, even though config.toml still says auto_review. Full Access, by contrast, always forces ApprovalsReviewer::User on its accept path (permission_popups.rs#L258-L283). The asymmetry — Full Access sets the reviewer explicitly, Custom restores it as "no change" — is the bug.

3. Why it survives restarts of the affected task but not fresh tasks

On resume, the server backfills the reviewer from the last persisted TurnContext/ThreadSettingsApplied entry in the rollout history, overriding config:

https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/app-server/src/request_processors/thread_processor.rs#L186-L203

Since the Custom switch never wrote a corrected reviewer into the thread's turn context (see 1), the last persisted value is still user, and every resume of that thread re-applies it. A fresh task has no history, reads config.toml, and correctly gets auto_review — exactly what you observed.

High-level fix outline

  1. When a permissions selection is applied in build_thread_settings_overrides, derive approvals_reviewer (and arguably approval_policy) from the config it already loads via load_for_cwd, unless the same request overrides them explicitly. This fixes desktop, TUI, and any other client in one place, and also writes the corrected reviewer into the turn context so the resume path (3) heals itself.
  2. Alternatively/additionally, extend ThreadSettingsOverrides with an explicit "reset to config" variant so clients can clear a stale override — Option::None currently conflates "no change" with "no opinion".
  3. TUI-side, permission_profile_selection_item could populate the reviewer from the effective config instead of None, mirroring what the built-in preset items already do.

A regression test for the exact transition (auto_review → Full Access → custom profile → assert reviewer restored, then resume the thread and assert again) would cover both the live-switch and the resume leak.