Codex Desktop Settings cannot save approval/sandbox config after plugin marketplace refresh updates config.toml

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

Summary

Codex Desktop Settings > Configuration shows:

Configuration was modified since last read. Fetch latest version and retry.

This happens when trying to change:

  • Approval policy (approval_policy)
  • Sandbox settings (sandbox_mode)

The actual config file is editable and valid. The failure appears to be an optimistic concurrency/version conflict in the Settings UI: the page reads a config.toml layer/version, but Codex plugin/marketplace refresh updates the same ~/.codex/config.toml shortly after, so the Settings page keeps using a stale expectedVersion when calling write-config-value.

Environment

  • Product: Codex Desktop app
  • App version: 26.519.31651
  • Bundle id: com.openai.codex
  • OS: macOS 26.5
  • Architecture: arm64
  • Config file: ~/.codex/config.toml

Reproduction Steps

  1. Open Codex Desktop.
  2. Open Settings > Configuration.
  3. Select User config.
  4. Try to change Approval policy, e.g. Never -> On request.
  5. Try to change Sandbox settings, e.g. Read only / Full access.
  6. Observe red error text under the control:

Configuration was modified since last read. Fetch latest version and retry.

Observed Behavior

  • The dropdown value may visually change, but saving fails with the error above.
  • Refreshing/reopening the settings page can still reproduce the issue if a background plugin/marketplace refresh modifies config.toml soon after the page reads it.
  • Editing ~/.codex/config.toml directly works.

Expected Behavior

Settings should either:

  • Refetch the latest config layer/version before writing, or
  • Retry the single-field upsert against the latest config after detecting a version mismatch, or
  • Ignore unrelated concurrent changes like marketplace last_updated when writing unrelated keys such as approval_policy and sandbox_mode.

Changing approval_policy and sandbox_mode should not fail because an unrelated marketplace timestamp changed.

Evidence

Sanitized ~/.codex/config.toml relevant fields:

sandbox_mode = "danger-full-access"
approval_policy = "on-request"

[marketplaces.openai-bundled]
last_updated = "2026-05-22T10:51:24Z"
source_type = "local"
source = "<codex-home>/.tmp/bundled-marketplaces/openai-bundled"

[marketplaces.openai-primary-runtime]
last_updated = "2026-05-21T18:51:32Z"
source_type = "local"
source = "<user-cache>/codex-runtimes/codex-primary-runtime/plugins/openai-primary-runtime"

Enabled plugin entries include:

[plugins."computer-use@openai-bundled"]
enabled = true

[plugins."browser@openai-bundled"]
enabled = true

[plugins."chrome@openai-bundled"]
enabled = true

Relevant log patterns around the failure:

app-server request: config/value/write
app_server.request otel.name="config/value/write" rpc.method="config/value/write"
/etc/codex/managed_config.toml not found
Managed preferences for com.openai.codex key requirements_toml_base64 not found
failed to refresh remote installed plugins cache
failed to warm featured plugin ids cache
configured non-curated plugin no longer exists in discovered marketplaces during cache refresh plugin="browser" marketplace="openai-bundled"
configured non-curated plugin no longer exists in discovered marketplaces during cache refresh plugin="chrome" marketplace="openai-bundled"
configured non-curated plugin no longer exists in discovered marketplaces during cache refresh plugin="computer-use" marketplace="openai-bundled"

Suspected Cause

The Settings page passes a stale expectedVersion into write-config-value for approval_policy / sandbox_mode writes. The version becomes stale because plugin/marketplace refresh updates the same user config file, especially [marketplaces.openai-bundled].last_updated.

A local inspection of the bundled frontend showed the Settings code calls write-config-value with expectedVersion:B.expectedVersion for these fields. If any unrelated writer modifies config.toml after the settings page read, the write is rejected and the UI does not recover automatically.

Workaround

Manually edit ~/.codex/config.toml, for example:

approval_policy = "on-request"
sandbox_mode = "danger-full-access"

Then restart Codex Desktop.

Notes

This does not appear to be caused by project-level instructions (AGENTS.md / agent.md) or project-level config. In this reproduction, the project had no .codex/config.toml and no project requirements.toml; logs also reported no managed config or MDM requirements key found.

View original on GitHub ↗

7 Comments

github-actions[bot] contributor · 1 month ago

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

  • #23264
  • #23169

Powered by Codex Action

jshaofa-ui · 1 month ago

openai/codex #24065: Desktop Settings Cannot Save Approval/Sandbox Config After Plugin — Fix

Issue Summary

After installing/activating a plugin in Codex Desktop, the Settings panel fails to save approval mode and sandbox configuration changes. User modifications to these settings appear to save (no error shown) but revert on reload or are silently ignored.

Severity: High — blocks security configuration, undermines sandbox isolation
Labels: bug, area:settings, area:sandbox, area:plugins
Competition: Zero

Root Cause Analysis

Primary Root Cause: Plugin Lifecycle Overwrites Settings Persistence Layer

The likely failure chain:

  1. User opens Settings → modifies approval mode or sandbox config
  2. Settings UI calls settingsManager.save() which writes to config file
  3. Plugin activation triggers a settings reload/re-initialization that:
  • Reads plugin defaults from plugin.json or manifest
  • Overwrites the in-memory settings store with plugin defaults
  • Does NOT re-read the user's persisted config
  1. The user's changes are lost because the plugin's initialization path clobbers the settings store

Alternative Root Cause: Settings Schema Conflict

The plugin may register a settings schema that conflicts with the built-in approval/sandbox settings:

  • Plugin declares overlapping setting keys
  • Settings manager merges schemas incorrectly
  • Approval/sandbox fields are dropped during merge

Failure Pattern:

User modifies approval/sandbox settings in UI
  → settingsManager.save() writes to disk ✓
  → Plugin activation fires
    → pluginManager.load() reads plugin manifest
    → plugin registers default settings
    → settingsManager.merge(pluginDefaults) ← OVERWRITES user settings
  → User's approval/sandbox config lost
  → On reload, settings read from disk (which was overwritten)
  → Reverts to plugin defaults or empty state

Why This Is Hard to Detect:

  • No error is shown to the user
  • Settings appear to save (no validation error)
  • The overwrite happens asynchronously after plugin load
  • User doesn't immediately notice until they try to use the affected feature

Proposed Fix

Fix 1: Plugin Settings Isolation (Primary Fix)

Plugins should NOT be able to overwrite core settings. Implement namespace isolation:

// settings-manager.ts
class SettingsManager {
  private readonly CORE_NAMESPACES = ['approval', 'sandbox', 'auth', 'network'];
  
  merge(pluginSettings: Record<string, unknown>, pluginId: string): void {
    for (const [key, value] of Object.entries(pluginSettings)) {
      const namespace = key.split('.')[0];
      
      // Prevent plugins from overwriting core settings
      if (this.CORE_NAMESPACES.includes(namespace)) {
        logger.warn(
          `Plugin ${pluginId} attempted to overwrite core setting "${key}". ` +
          `Core settings are protected and cannot be modified by plugins.`
        );
        continue; // Skip this setting
      }
      
      this.store[key] = value;
    }
  }
}

Fix 2: Save-After-Load Ordering

Ensure user settings are re-read after plugin initialization:

// app-init.ts
async function initializeApp(): Promise<void> {
  // 1. Load user settings FIRST (from disk)
  const userSettings = await settingsManager.loadUserSettings();
  
  // 2. Initialize plugins (which may register their own defaults)
  await pluginManager.initialize();
  
  // 3. Re-apply user settings on top (user wins over plugin defaults)
  await settingsManager.applyUserSettings(userSettings);
  
  // 4. Only then merge plugin defaults for settings the user hasn't set
  await settingsManager.mergePluginDefaults({ userWins: true });
}

Fix 3: Settings Change Coalescing

Defer settings writes to avoid race conditions with plugin initialization:

// settings-manager.ts
class SettingsManager {
  private saveQueue: Promise<void> = Promise.resolve();
  private pendingChanges: Map<string, unknown> = new Map();
  private saveTimer: NodeJS.Timeout | null = null;
  
  async set(key: string, value: unknown): Promise<void> {
    this.pendingChanges.set(key, value);
    
    // Debounce saves to avoid race with plugin init
    if (this.saveTimer) clearTimeout(this.saveTimer);
    this.saveTimer = setTimeout(() => this.flushChanges(), 100);
  }
  
  private async flushChanges(): Promise<void> {
    if (this.pendingChanges.size === 0) return;
    
    const changes = new Map(this.pendingChanges);
    this.pendingChanges.clear();
    
    // Write atomically
    await this.writeToDisk(changes);
  }
}

Fix 4: Settings Integrity Verification

Add a post-save verification step:

async function saveSettings(settings: Settings): Promise<void> {
  const checksum = hashSettings(settings);
  await writeToDisk(settings);
  
  // Verify round-trip
  const reloaded = await readFromDisk();
  if (hashSettings(reloaded) !== checksum) {
    logger.error('Settings integrity check failed after save — possible plugin interference');
    // Restore from in-memory copy
    await writeToDisk(settings);
  }
}

function hashSettings(settings: Settings): string {
  return crypto.createHash('sha256')
    .update(JSON.stringify(settings, Object.keys(settings).sort()))
    .digest('hex');
}

Test Plan

Unit Tests

  1. Plugin overwrites core setting: Plugin tries to set approval.mode → verify it's rejected
  2. User settings survive plugin load: Save settings, load plugin, verify settings unchanged
  3. Settings checksum mismatch: Simulate disk corruption → verify restoration from memory
  4. Concurrent save + plugin load: Race condition test → verify no data loss

Integration Tests

  1. Install plugin → change settings → restart: Verify settings persist
  2. Change settings → install plugin → verify: Verify settings not overwritten
  3. Multiple plugins: Install 3 plugins, verify core settings untouched
  4. Plugin uninstall: Uninstall plugin, verify settings remain correct

Regression Tests

  1. Normal settings save: Change approval mode, restart → verify persistence
  2. Sandbox config: Change sandbox settings, restart → verify persistence
  3. Plugin-specific settings: Plugin settings should still work normally

Risk Assessment

| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| Plugin breaks due to core setting overwrite being blocked | Low | Medium | Log clear warning; provide plugin migration guide |
| Settings save latency from debouncing | Low | Low | 100ms debounce is imperceptible; use immediate save for critical settings |
| Checksum collision | Very Low | Low | SHA-256 collision probability is negligible |
| Breaking change for plugins that legitimately need core settings | Very Low | Low | No legitimate use case; core settings should never be plugin-modifiable |

Overall Risk: LOW — all changes are defensive and improve security posture. The plugin isolation fix is the most impactful and has no legitimate negative side effects.

References

bradleyhodges · 1 month ago

Also experiencing this issue with Codex 26.521.10419 on Windows.

ys770 · 1 month ago

same :(

marcfilleul · 1 month ago

Same here on Mac OS. Codex version 26.519.41501. Fresh install.
When editing config.toml and launching Codex, these two settings are deleted from the config file.

MisterRound · 1 month ago

Adding a current Windows Desktop + WSL data point from the 26.527 build family.

The same config-version conflict appeared while trying to disable the Computer use plugin from Settings. The UI showed a toast like Failed to update plugin, and Desktop logs showed the plugin state write failed because the config version was stale:

Failed to update plugin enabled state
Configuration was modified since last read. Fetch latest version and retry.

Sanitized environment shape:

Product: Codex Desktop Windows Store app
Package family: OpenAI.Codex_26.527.x_x64__2p2nqsd0c76g0
Mode: Desktop configured to run app-server in WSL
Codex home: Windows profile path mounted into WSL as /mnt/c/Users/<user>/.codex

The surrounding logs had active bundled marketplace/plugin reconciliation, including mixed Windows/WSL path symptoms tracked in #24268. That matches this issue's suspected cause: Settings has an expectedVersion, background plugin/marketplace refresh mutates config.toml, and then the user-facing write is rejected instead of re-reading/retrying.

Suggested fix still looks the same: on configVersionConflict, Settings should refetch the latest config version and retry the single-field write, or batch/serialize config updates so unrelated marketplace/plugin timestamp changes do not break user settings changes.

summer521521 · 1 month ago

Adding a current Windows Store Desktop data point from the 26.602 build family, with a local file-watch reproduction.

Environment:

Product: Codex Desktop Windows Store app
App package: OpenAI.Codex_26.602.4764.0_x64__2p2nqsd0c76g0
Desktop app reported runtime: 26.602.40724 in generated config env
OS: Windows
Codex home: C:\Users\<user>\.codex
Config file: C:\Users\<user>\.codex\config.toml
No project-level .codex/config.toml in the active workspace
No separate config.toml found under ProgramData, AppData\Roaming, or AppData\Local

Observed UI behavior:

  • Settings > Configuration > User config shows the red error text:
Configuration was modified since last read. Fetch latest version and retry.
  • The error appears when changing either:
  • Approval policy
  • Sandbox settings
  • Restarting Codex Desktop does not reliably clear the problem.

Important local observation: the config file is not read-only and the UI can write it. A 60-second PowerShell watcher on config.toml showed the Settings UI successfully wrote the approval setting, then a second unrelated Codex background write changed the same file shortly after.

Watcher output, sanitized:

WATCH_START 2026-06-06 15:31:19 hash=55963D4A55B0666831B6120E7947085E27377304A3D8828EF2BDAE8718B8D776
CHANGE 2026-06-06 15:31:26.770 LastWrite=2026-06-06 15:31:26.738 hash=DA28FBFBE35BD13B1818F448279C98DE40FD842278A8C50E6585FB5FF90093C4
  approval_policy :: 'approval_policy = "never"' -> 'approval_policy = "on-request"'
CHANGE 2026-06-06 15:31:37.973 LastWrite=2026-06-06 15:31:37.804 hash=A38B510A85D3643D494610132DA6D9C5D8627C9600574622E81206A7A0695355
WATCH_END 2026-06-06 15:32:19 hash=A38B510A85D3643D494610132DA6D9C5D8622E81206A7A0695355

After the second write, the relevant top-level fields were still:

approval_policy = "on-request"
sandbox_mode = "danger-full-access"

The unrelated second write added/updated a feature flag in the same file:

[features]
workspace_dependencies = true

This supports the suspected expectedVersion / optimistic-concurrency failure: the user-facing settings write can succeed, but another Codex background writer mutates config.toml while the Settings page is still holding an older version. The Settings page then reports a stale-version conflict instead of refetching and retrying the single-field write.

Additional state mismatch observed in local app state:

  • config.toml top-level values after the UI write:
approval_policy = "on-request"
sandbox_mode = "danger-full-access"
  • .codex-global-state.json still had the current thread cached with a different permission shape:
{
  "approvalPolicy": "on-request",
  "sandboxPolicy": {
    "type": "workspaceWrite",
    "writableRoots": ["C:\\Users\\<user>\\Documents\\Codex"],
    "networkAccess": false
  }
}

So this does not look like filesystem permissions or invalid TOML. It looks like multiple app-side config/state writers are not coordinated, and the Settings UI does not recover from a version conflict caused by unrelated config updates.