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.tomlalready containsapprovals_reviewer = "user",approval_policy = "never", andsandbox_mode = "danger-full-access".~\.codex\automation.config.tomlcontains 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
.tmpor.lockfiles. - The error persists across startups.
Related issues:
- #26077:
batchWritehides the underlying write error. - #36647: the Full Access path appears to write
approvals_reviewerunconditionally.
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.
2 Comments
The
config/batchWrite failedon Full Access is basically cosmetic, the config's already persisted,approvals_reviewer = \"user\"andapproval_policy = \"never\"are sitting in~\\.codex\\config.tomland 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.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::UpdateApprovalsRevieweras 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/batchWritewithreplace_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 = policybefore 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_editsdiffs each edit against the current user layer and only touches disk when a value actually changes — edits are collected onlyif original_value != updated_value, and the writer runs onlyif !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 yourconfig.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;approvals_revieweris covered by arequirements.toml/managed layer, the request fails withConfigRequirementReadonly("approvals_revieweris managed by requirements and cannot be changed");validate_config→try_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_codeindata(map_errorinconfig_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_batchwraps 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 withDisplay({err}/error = %err), which prints only the outermost context. So the diagnostic reaches neither the user nor the logs — aRUST_LOGcapture is equally blind, which makes this class of report undiagnosable as filed. Formatting with{err:#}(or walkingerr.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
requirements.tomlor managed config layer under%USERPROFILE%\.codex(or an enterprise-managed location) that mentionsapprovals_revieweror approval policy.codex --strict-configonce: if a layer fails strict validation, startup will now name the offending key instead of silently tolerating it.High-level fix outline
UpdateApprovalsReviewerhandler or before sending the event inapproval_preset_actions).config_write_error_code) in both the history cell and the log line. This half is arguably more valuable, since it covers every otherwrite_config_batchcaller too.