/experimental saves on Esc and writes flags the user never toggled

Open 💬 1 comment Opened Aug 11, 2026 by apieum

What happens

Opening /experimental, then pressing Esc without touching anything, writes
every experimental feature flag to the config file. The popup's own footer
reads "press space to select or enter to save for next conversation", so Esc is
expected to discard.

Steps to reproduce

  1. Start with a config.toml that has no [features] section.
  2. Run codex, type /experimental, press Enter.
  3. Press Esc without toggling anything.
  4. Look at ~/.codex/config.toml.

Expected: the file is unchanged.
Actual: the file now carries an entry for every feature listed in the popup.

The same happens when a single flag is toggled and saved with Enter: the write
carries all the listed features, not just the one that changed. On a machine
where a flag was left at a non-default value on purpose, opening the popup once
is enough to pin every other flag to its current value.

Root cause

ExperimentalFeaturesView::on_ctrl_c in
codex-rs/tui/src/bottom_pane/experimental_features_view.rs sends
AppEvent::UpdateFeatureFlags with the full feature list, then marks the view
complete. handle_key_event routes both keymap.accept and keymap.cancel to
that same method, so cancelling takes the saving path. The comment on the
method (// Save the updates) suggests it was written for acceptance and later
reused for cancellation.

Suggested fix

Split the two paths: cancel (and Ctrl-C) closes without emitting anything,
accept emits only the features whose value differs from the state the popup
was opened with. Keeping the opening state in the view is enough; no new
plumbing is needed.

Context

I ran into this while working on per-scope configuration in my own fork, where
a popup writing flags nobody toggled is easy to spot. Happy to open a separate
issue for that work if there is interest — this report is only about the popup
behaviour, which reproduces on a stock build of main.

The change and its tests are ready on a branch, if the team wants it:
https://github.com/openai/codex/compare/main...apieum:codex:fix/experimental-popup-discards

View original on GitHub ↗

1 Comment

jdcodes1 · 9 days ago

Confirmed unchanged on main @ 1f41cc5d92 — both halves of your root cause are verbatim in the current code:

https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/tui/src/bottom_pane/experimental_features_view.rs#L185-L211

accept and cancel route to the same on_ctrl_c, whose body (commented "Save the updates") sends UpdateFeatureFlags with every listed feature's current state. So Esc saves, and any save pins all flags, exactly as reported. Your suggested fix is right on both axes: cancel/Ctrl-C should mark complete without emitting; accept should emit only features whose value differs from the snapshot taken at open (diffing against an initial_enabled captured per item is a few lines — the item struct already carries per-feature state).

Worth noting the second half is the more damaging one: writing all flags at their current effective values freezes defaults into config.toml, so users silently stop tracking upstream default changes for every experimental feature after one popup visit — that's the "flags the user never toggled" persistence and it outlives the session. The diff-only emit fixes both symptoms at once.