Goal auto-continuations can downgrade Full Access threads to read-only/on-request while UI still shows Full Access

Resolved 💬 10 comments Opened May 24, 2026 by jianzhangg Closed Jun 10, 2026
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Summary

A Goal thread that is persisted and displayed as Full Access / no approvals can have automatic continuation turns launched with a read-only sandbox, on-request approvals, and a managed permission profile.

The visible state and persisted thread state indicate Full Access, but actual tool execution is restricted. This causes EPERM failures and approval paths during unattended Goal runs.

Environment

  • Codex Desktop for macOS
  • App version: 26.519.41501, build 3044
  • Bundled CLI: codex-cli 0.133.0-alpha.1
  • macOS: 15.7.4
  • Goal feature enabled

What I expected

Goal automatic continuations should inherit the thread's effective permission context. If a Goal thread is Full Access with approvals disabled, its automatic continuation turns should also run with Full Access and no approvals.

Alternatively, if the runtime intentionally downgrades the permissions, the UI should show the effective downgrade before the continuation runs.

What happened

The thread metadata / persisted state showed Full Access:

thread_id=019e53bb-dda1-7ff3-ae85-3c3fd33b2119
sandbox_policy={"type":"danger-full-access"}
approval_mode=never

However, automatic Goal continuation turns in the rollout used a downgraded runtime context:

approval_policy=on-request
sandbox_policy={"type":"read-only"}
permission_profile=managed

This happened on automatic Goal continuation / resume turns, not on normal user-submitted turns.

Timeline / evidence

2026-05-24T05:44:39Z  approval_policy=never      sandbox_policy={"type":"danger-full-access"}  permission_profile=disabled
2026-05-24T05:46:33Z  approval_policy=on-request sandbox_policy={"type":"read-only"}          permission_profile=managed
2026-05-24T05:52:16Z  approval_policy=on-request sandbox_policy={"type":"read-only"}          permission_profile=managed
2026-05-24T05:54:21Z  approval_policy=on-request sandbox_policy={"type":"read-only"}          permission_profile=managed
2026-05-24T06:03:17Z  approval_policy=on-request sandbox_policy={"type":"read-only"}          permission_profile=managed
2026-05-24T06:24:09Z  approval_policy=on-request sandbox_policy={"type":"read-only"}          permission_profile=managed
2026-05-24T06:25:55Z  approval_policy=never      sandbox_policy={"type":"danger-full-access"}  permission_profile=disabled

The downgraded entries were automatic Goal continuations (summary=auto).

Error examples

Attempts to save local artifacts failed with EPERM during the downgraded Goal continuation turns, including a workspace file and temporary files:

EPERM: operation not permitted, open '<workspace-path>/29-current-debt-unpay-header-chain-field-final.png'
EPERM while writing /tmp/pf5665-write-test.txt
EPERM while writing /var/folders/.../pf5665-write-test.txt

A later command attempted to use an escalation path despite the UI/thread state indicating Full Access, and it failed as an approval rejection:

Rejected("rejected by user")

Impact

  • Long-running Goal workflows become unreliable when unattended.
  • The UI can say or imply Full Access while the actual runtime behaves like the default/read-only profile.
  • The assistant may incorrectly ask for or attempt approvals in a thread that should not require them.
  • File-producing tasks can stall on EPERM even when the thread was started and persisted as Full Access.

Related issues

This seems related to, but more specific than:

  • #22090 (/goal continuation uses stale permission context after permissions changes)
  • #23105 (Full Access /goal session falling back to sandboxed execution)
  • #21839 (previously-existing Full Access sessions requiring approvals)

The distinguishing detail here is that the persisted thread state still shows danger-full-access / approval_mode=never, while automatic Goal continuation turn_context entries are actually launched as read-only / on-request / managed.

View original on GitHub ↗

10 Comments

jianzhangg · 1 month ago

Additional observation: in my local usage, this seems to occur mostly after Codex updates/restarts, or after some other condition that causes the app/session to exit and later resume.

I have not seen this happen while keeping the same Codex app process running continuously without restarting. The problematic cases so far are associated with a restart/resume boundary, where the thread still appears to be Full Access afterward but the Goal auto-continuation can run with the downgraded read-only / on-request / managed context.

github-actions[bot] contributor · 1 month ago

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

  • #23105
  • #23958
  • #24270

Powered by Codex Action

etraut-openai contributor · 1 month ago

Thanks for reporting. This is a known issue, and we're working on a fix.

In the meantime, you can work around it by issuing a dummy prompt after you change the permissions of the thread but before you create a goal. The same goes for other thread settings like the model, reasoning effort, and fast mode.

jshaofa-ui · 1 month ago

Solution: Fix Goal Auto-Continuations Downgrading Full Access Threads

Issue: openai/codex #24300
Labels: bug, sandbox, app, session, automations
Component: Goal Automation + Sandbox Permission Inheritance
Severity: HIGH (Breaks unattended Goal execution)

---

1. Problem Statement

A Goal thread that is persisted and displayed as "Full Access / no approvals" launches automatic continuation turns with:

  • Read-only sandbox (instead of full access)
  • On-request approvals (instead of no approvals)
  • Managed permission profile (instead of Full Access)

Impact: This causes EPERM (permission denied) failures and approval prompts during unattended Goal runs, breaking the automation loop.

---

2. Root Cause Analysis

2.1 Architecture Overview

Goal Thread (Full Access)
    ↓ (persists: sandbox = "full_access", approvals = "none")
GoalController (manages auto-continuation)
    ↓ (spawns continuation turn)
TurnExecutor (executes next turn)
    ↓ (inherits: sandbox = "read_only" ❌, approvals = "on_request" ❌)

2.2 Likely Root Cause

Permission Profile Not Inherited in Auto-Continuation Path

When the GoalController spawns an auto-continuation turn, it creates a new execution context but fails to copy the parent thread's sandbox and permission settings. Instead, it uses default values:

  • Default sandbox: read_only (safe default for manual turns)
  • Default approvals: on_request (safe default for manual turns)

The Full Access settings from the parent thread are not propagated to the continuation turn.

Code Flow:

1. User creates Goal thread with Full Access
2. GoalController stores: thread.sandbox = "full_access", thread.approvals = "none"
3. First turn executes correctly with Full Access
4. GoalController decides to auto-continue
5. GoalController.spawnContinuation() creates new TurnContext
6. TurnContext defaults to sandbox="read_only", approvals="on_request" ← BUG
7. Turn executes with wrong permissions → EPERM failures

---

3. Proposed Fix

3.1 Primary Fix: Inherit Permission Profile in Auto-Continuation

File: src/goal/goal-controller.ts (or equivalent)

class GoalController {
  async spawnContinuationTurn(thread: Thread): Promise<TurnContext> {
    // BEFORE (buggy):
    // const context = this.turnFactory.create({
    //   threadId: thread.id,
    //   // sandbox and approvals use defaults
    // });
    
    // AFTER (fixed):
    const context = this.turnFactory.create({
      threadId: thread.id,
      // Inherit sandbox and approval settings from parent thread
      sandbox: thread.sandbox ?? 'full_access',
      approvals: thread.approvals ?? 'none',
      permissionProfile: thread.permissionProfile,
      // Also inherit other relevant settings
      model: thread.model,
      systemPrompt: thread.systemPrompt,
    });
    
    return context;
  }
}

3.2 Secondary Fix: Add Permission Validation

Add a validation step that ensures auto-continuation turns match the thread's permission profile:

function validateContinuationPermissions(
  thread: Thread, 
  context: TurnContext
): void {
  const mismatches: string[] = [];
  
  if (context.sandbox !== thread.sandbox) {
    mismatches.push(`sandbox: expected ${thread.sandbox}, got ${context.sandbox}`);
  }
  if (context.approvals !== thread.approvals) {
    mismatches.push(`approvals: expected ${thread.approvals}, got ${context.approvals}`);
  }
  
  if (mismatches.length > 0) {
    // Log warning and correct before execution
    console.warn(
      `Goal auto-continuation permission mismatch for thread ${thread.id}:`,
      mismatches.join('; ')
    );
    
    // Auto-correct: use thread settings
    context.sandbox = thread.sandbox;
    context.approvals = thread.approvals;
    context.permissionProfile = thread.permissionProfile;
  }
}

3.3 Tertiary Fix: Add UI Consistency Check

Ensure the UI accurately reflects the actual permissions:

// When rendering the thread status:
function renderThreadStatus(thread: Thread): ThreadStatus {
  const actualPermissions = getActualPermissions(thread.currentTurn);
  
  return {
    displayAccess: thread.sandbox,  // What user set
    actualAccess: actualPermissions.sandbox,  // What's actually used
    isConsistent: thread.sandbox === actualPermissions.sandbox,
    // If inconsistent, show a warning indicator
    warning: thread.sandbox !== actualPermissions.sandbox 
      ? 'Permission mismatch detected' 
      : undefined,
  };
}

---

4. Testing Strategy

Unit Tests

  1. Full Access thread auto-continuation → continuation uses Full Access sandbox
  2. On-Request thread auto-continuation → continuation uses On-Request approvals
  3. Read-Only thread auto-continuation → continuation uses Read-Only sandbox
  4. Mixed permissions → all permission fields inherited correctly

Integration Tests

  1. Goal with Full Access runs 5 auto-continuations → all succeed without EPERM
  2. Goal with On-Request runs 3 auto-continuations → each prompts for approval
  3. Permission change mid-goal → subsequent continuations use new settings

Regression Tests

  1. Verify the specific EPERM error from the issue no longer occurs
  2. Verify UI shows consistent permissions during auto-continuation

---

5. Impact Assessment

  • Severity: HIGH — breaks unattended Goal execution completely
  • User Impact: Goals fail silently or require manual intervention, defeating automation purpose
  • Risk of Fix: Low — inheriting parent settings is the correct behavior
  • Breaking Changes: None — this fixes broken behavior

---

6. Files to Modify

| File | Change |
|------|--------|
| src/goal/goal-controller.ts | Inherit permission profile in spawnContinuationTurn |
| src/goal/permission-validator.ts (new) | Add permission validation for continuations |
| src/app/thread-status-renderer.ts | Add consistency check and warning indicator |
| src/types/turn-context.ts | Ensure all permission fields are inheritable |
| tests/goal/auto-continuation.test.ts | Add regression tests |

---

7. Estimated Effort

  • Implementation: 3-5 hours
  • Testing: 2-3 hours
  • Review: 1 hour
  • Total: 6-9 hours
andkondev · 1 month ago
In the meantime, you can work around it by issuing a dummy prompt _after you change the permissions of the thread but before you create a goal_. The same goes for other thread settings like the model, reasoning effort, and fast mode.

What if there was no change in permissions, or model, reasoning, fast mode? I'm having this issue in a thread where I just set a goal without changing any of those settings...

etraut-openai contributor · 1 month ago
I'm having this issue in a thread where I just set a goal without changing any of those settings...

Is it a new thread where you haven't sent any user prompts? Then this could occur. Sending a user prompt should work around the problem. If you're seeing this for a thread where you've already sent at least one user prompt and have not subsequently changed the permissions, let me know because that behavior would be explained by our current understanding of the problem.

andkondev · 1 month ago
If you're seeing this for a thread where you've already sent at least one user prompt and have not subsequently changed the permissions

Yes: already started thread, lots of replies back and forth, no permissions or other settings changed... and goal keeps asking for permissions even with "Full access." What more info do you need (I posted version etc in the closed duplicate thread.)

andrewkangkr · 1 month ago

I can reproduce a closely related variant of this on Windows Codex Desktop, and in my case it is not limited to Goal auto-continuations.

Environment

  • Codex Desktop app: OpenAI.Codex_26.527.7698.0_x64__2p2nqsd0c76g0
  • Platform: Windows
  • Active saved workspace root in app state: C:\Users\강인철\Desktop\AX\code

What I am seeing

The app/UI flow led me to believe I was operating in a Full Access-like mode, but newly created threads still persisted as:

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

This reproduces even for fresh test threads that do nothing except reply once and stop.

Why this seems related but broader than the current issue

This looks adjacent to the Full Access / visible-state mismatch in this issue, but I can reproduce it on ordinary new thread creation / handoff creation, not only on automatic Goal continuation turns.

In other words:

  • existing or visible app state suggests Full Access was selected at some point
  • but new threads still get created with on-request/workspaceWrite
  • approval prompts then come from those new threads

Repro evidence from my local app state

Host-level persisted mode in C:\Users\강인철\.codex\.codex-global-state.json:

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

Fresh / recent threads I created while investigating:

  • 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

One extra datapoint:

  • among the currently stored thread states on this machine, I found only 1 thread with approvalPolicy = never, while 60 were on-request

So the behavior is not a one-off for a single stuck thread; it looks like new threads are systematically defaulting back to the guardian/on-request path.

Minimal repro I used

  1. Open the standalone Windows Codex Desktop app.
  2. Work in a normal thread until permission behavior appears to be Full Access / no-approval.
  3. Create a new thread from that session, or hand off into a new thread.
  4. Even if the new thread only answers once and does no real work, inspect persisted thread state.
  5. Observe that the new thread is stored as on-request / workspaceWrite / guardian_subagent.
  6. Approval prompts then come from those newly created threads.

Why this may be useful for triage

This seems different from issue patterns where:

  • the runtime context is truly never + danger-full-access but the approval UI still fires anyway

In my case, the persisted thread state itself already shows the downgrade:

  • the visible workflow suggests Full Access was selected
  • but newly created threads are actually persisted as on-request + workspaceWrite

So this may be a thread creation / permission inheritance bug on Windows Desktop, or a case where the UI selection is not being committed to the host/thread creation path.

duncanmcl · 1 month ago

I can reproduce a related VS Code variant that may be useful for debugging.

In a thread, the first bad permission transition I found happened before any thread_goal_updated event in the session log:

  • 2026-05-19T22:18:53Z: approval_policy=never, sandbox_policy=danger-full-access, permission_profile=disabled
  • 2026-05-19T22:49:17Z: next normal user-message turn changed to approval_policy=on-request, sandbox_policy=workspace-write, permission_profile=managed
  • first thread_goal_updated event was later: 2026-05-30T03:26:43Z

So in this case the Goal does not appear to have created the bad permission state. The thread first entered workspace-write/on-request during a normal old-thread turn, and later Goal auto-continuation appears to have inherited that per-thread state.

Current local VS Code state has separate stores for:

  • global Full Access: agent-mode-by-host-id.local = full-access
  • per-thread/heartbeat permissions: heartbeat-thread-permissions-by-id

Relevant local VS Code extension code paths I found, shown as simplified pseudocode from the minified bundle:

  1. Composer can send a turn without explicit permission overrides:

webview/assets/composer-CotcHxTB.js

g = shouldSendPermissionOverrides ? await readConfig(...) : null
_ = g == null ? null : makePermissions(...)
... _ == null ? {} : {
  approvalPolicy: _.approvalPolicy,
  approvalsReviewer: _.approvalsReviewer,
  sandboxPolicy: _.sandboxPolicy
}
  1. The turn-start helper then inherits permissions from in-memory thread state, or falls back:

webview/assets/app-server-manager-signals-B8yJv95l.js

approvalPolicy =
  request.approvalPolicy
  ?? latestThreadSettings?.approvalPolicy
  ?? latestTurn?.params.approvalPolicy
  ?? currentPermissions?.approvalPolicy
  ?? computedDefaultPermissions.approvalPolicy

sandboxPolicy =
  request.sandboxPolicy
  ?? latestThreadSettings?.sandboxPolicy
  ?? latestTurn?.params.sandboxPolicy
  ?? currentPermissions?.sandboxPolicy
  ?? computedDefaultPermissions.sandboxPolicy
  1. Heartbeat/Goal watcher can persist runtime permissions per thread:

webview/assets/local-conversation-stream-role-product-event-CeOlewiE.js

i = currentPermissions(...)
if (i != null) {
  set(heartbeatThreadPermissionsById, threadId, i)
}

This suggests the fix may need to handle stale or unintended per-thread permission state, not only Goal creation immediately after changing permissions.

etraut-openai contributor · 1 month ago

This will be addressed in the next release of the app.