TUI: 'Failed to save approvals reviewer: config/batchWrite failed' on every Full Access startup despite fully persisted config (0.147.0, Windows)

Open 💬 2 comments Opened Aug 16, 2026 by apnascimentto2024

Summary

On codex-cli 0.147.0 on Windows 11 (npm install), every startup in Full Access shows this error immediately after Permissions updated to Full Access:

Failed to save approvals reviewer: config/batchWrite failed

The session still clearly starts in Full Access / YOLO mode, so the profile config is loading. This looks like a redundant startup persistence attempt that fails noisily even though the setting is already persisted.

Forensics

  • ~\.codex\config.toml already contains approvals_reviewer = "user", approval_policy = "never", and sandbox_mode = "danger-full-access".
  • ~\.codex\automation.config.toml contains the same persisted values.
  • ACLs were checked and normalized: owner has FullControl, there are no Deny ACEs, and directory inheritance is clean.
  • Global state JSON is valid.
  • There are no leftover .tmp or .lock files.
  • The error persists across startups.

Related issues:

  • #26077: batchWrite hides the underlying write error.
  • #36647: the Full Access path appears to write approvals_reviewer unconditionally.

Request

Please make the startup write conditional, skipping it when approvals_reviewer is already persisted, and/or surface the underlying write error instead of only reporting config/batchWrite failed.

No secrets are included here; paths are limited to ~\.codex.

View original on GitHub ↗

2 Comments

MilkyWay008 · 11 days ago

The config/batchWrite failed on Full Access is basically cosmetic, the config's already persisted, approvals_reviewer = \"user\" and approval_policy = \"never\" are sitting in ~\\.codex\\config.toml and it still boots into Full Access fine. It's a known upstream quirk, the Full Access path writes the reviewer unconditionally so it fires every startup without actually breaking anything. I'd just check the file isn't locked by Defender or OneDrive sync, then ignore it, or pin an older 0.14x if it keeps bugging you.

jdcodes1 · 11 days ago

I traced this through the code on main (1f41cc5d92); the redundant-write hypothesis is correct, and there is a concrete reason the ACL/lock forensics come up empty.

1. The write is unconditional — confirmed

Accepting Full Access always emits AppEvent::UpdateApprovalsReviewer as part of the preset actions:

https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/tui/src/chatwidget/permission_popups.rs#L258-L283

and the handler unconditionally issues config/batchWrite with replace_config_value("approvals_reviewer", ...) — there is no check against the already-persisted value:

https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/tui/src/app/event_dispatch.rs#L2165-L2188

Note for a fix: the handler assigns self.config.approvals_reviewer = policy before persisting, so an "is it already persisted?" guard has to compare before that assignment.

2. Why the file forensics show nothing: no file write actually happens

Server-side, apply_edits diffs each edit against the current user layer and only touches disk when a value actually changes — edits are collected only if original_value != updated_value, and the writer runs only if !config_edits.is_empty():

https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/app-server/src/config_manager_service.rs#L358-L430

Since approvals_reviewer = "user" is already persisted in your config.toml, this batchWrite performs zero I/O on the file. Checking ACLs, Deny ACEs, lock/tmp files etc. can never surface the cause, because the failure happens in the pre-write pipeline instead. The candidates, in the order they run:

  • load_thread_agnostic_config() — re-reads every config layer (user, profile, managed, requirements); any read/parse error on any layer fails the whole request;
  • the requirements check — if approvals_reviewer is covered by a requirements.toml/managed layer, the request fails with ConfigRequirementReadonly ("approvals_reviewer is managed by requirements and cannot be changed");
  • re-validation of the user layer alone (validate_configtry_into::<ConfigToml>()) — this deserializes the raw user layer by itself, whereas startup deserializes the merged effective config after CLI/profile overrides are applied, so a bad raw value that startup shadows can still fail every write.

All of these match the reported symptom: the session still boots into Full Access fine, yet the write fails on every startup, and nothing on-disk looks wrong.

3. The underlying error is genuinely discarded (confirms #26077)

The server returns a descriptive JSON-RPC error — a human-readable message plus a machine-readable config_write_error_code in data (map_error in config_processor.rs). The client-side error type even formats all of it:

https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/app-server-client/src/lib.rs#L139-L154

But write_config_batch wraps that in an eyre context (.wrap_err("config/batchWrite failed in TUI"), config_update.rs#L163), and both the history cell and the tracing call at the handler format with Display ({err} / error = %err), which prints only the outermost context. So the diagnostic reaches neither the user nor the logs — a RUST_LOG capture is equally blind, which makes this class of report undiagnosable as filed. Formatting with {err:#} (or walking err.chain()) at those two sites would immediately reveal the real cause for everyone hitting this.

Diagnostics that could pin down which pre-write step fails on your machine

  • Check for a requirements.toml or managed config layer under %USERPROFILE%\.codex (or an enterprise-managed location) that mentions approvals_reviewer or approval policy.
  • Run codex --strict-config once: if a layer fails strict validation, startup will now name the offending key instead of silently tolerating it.

High-level fix outline

  1. Skip the emit/persist when the reviewer value is unchanged (guard in the UpdateApprovalsReviewer handler or before sending the event in approval_preset_actions).
  2. Surface the full error chain (message + config_write_error_code) in both the history cell and the log line. This half is arguably more valuable, since it covers every other write_config_batch caller too.