`app-server` drops global `-c` overrides when another `-c` appears after the subcommand
What version of Codex CLI is running?
codex-cli 0.147.0
What subscription do you have?
ChatGPT Pro
Which model were you using?
Not applicable. The reproduction only calls experimentalFeature/list and does not start a turn.
What platform is your computer?
Linux 6.18.44 x86_64 unknown (NixOS)
What terminal emulator and version are you using (if applicable)?
Ghostty 1.3.1
Codex doctor report
Not included. The reproduction uses an empty temporary CODEX_HOME, does not require authentication, and does not start a model turn.
What issue are you seeing?
codex app-server silently drops every global -c / --config override placed before the app-server subcommand when any -c is also placed after the subcommand.
This works:
codex -c 'features.default_mode_request_user_input=true' app-server
experimentalFeature/list reports:
{
"name": "default_mode_request_user_input",
"enabled": true,
"defaultEnabled": false
}
Adding an unrelated override after app-server causes the earlier override to disappear:
codex -c 'features.default_mode_request_user_input=true' app-server \
-c 'suppress_unstable_features_warning=true'
The same request now reports:
{
"name": "default_mode_request_user_input",
"enabled": false,
"defaultEnabled": false
}
This affects more than feature flags. Settings such as model_provider, custom model_providers, model defaults, approval policy, and sandbox mode can all be silently discarded.
The documented configuration precedence says CLI flags and --config overrides have the highest precedence: https://learn.chatgpt.com/docs/config-file/config-basic#configuration-precedence
What steps can reproduce the bug?
- Create a clean Codex home:
``bash``
export CODEX_HOME="$(mktemp -d)"
- Start app-server with only a global override:
``bash``
codex -c 'features.default_mode_request_user_input=true' app-server
- Send these JSON-RPC messages over stdin, waiting for the initialize response before sending
initialized:
``json``
{"id":1,"method":"initialize","params":{"clientInfo":{"name":"config-repro","version":"0"},"capabilities":{"experimentalApi":true}}}
{"method":"initialized"}
{"id":2,"method":"experimentalFeature/list","params":{"cursor":null,"limit":null,"threadId":null}}
- Find
default_mode_request_user_inputin responseid: 2. It hasenabled: true.
- Stop app-server and repeat with an unrelated post-subcommand override:
``bash``
codex -c 'features.default_mode_request_user_input=true' app-server \
-c 'suppress_unstable_features_warning=true'
- Send the same JSON-RPC messages.
default_mode_request_user_inputnow hasenabled: false.
What is the expected behavior?
Codex should retain both sets of overrides in CLI order. The second command should apply both:
features.default_mode_request_user_input=true
suppress_unstable_features_warning=true
At minimum, Codex should not silently discard valid overrides based on whether they appear before or after the subcommand.
Additional information
Moving every override after app-server works around the bug:
codex app-server \
-c 'features.default_mode_request_user_input=true' \
-c 'suppress_unstable_features_warning=true'
Possibly related to #26436, which added app-server -c support, but that PR does not cover mixing overrides before and after the subcommand.
1 Comment
Traced on
main@ 1f41cc5d92 —app-serveris missing the root/subcommand override merge that every other codex subcommand performs, and the drop you measured is the documented clap behavior for multi-value global args falling into that gap.The in-repo convention.
-c/--configis declaredglobal = true(utils/cli/src/config_override.rs), and subcommands that accept it flatten their ownCliConfigOverridesand then explicitly merge the root-level flags in front withprepend_config_flags/prepend_root_overrides("Prepend root-level config flags so they have lower precedence than command-specific flags", config_override.rs#L40-L45).main.rsdoes this in ~16 dispatch arms (tui, exec, mcp, plugin, …).The
app-serverarm doesn't.AppServerCommandhas no flattenedconfig_overridesfield at all (cli/src/main.rs#L525-L541), and its dispatch arm passes the root-derivedroot_config_overridesstraight intorun_main_with_transport_optionswith no merge (#L1131-L1185). So post-subcommand-coccurrences are captured by clap's global-arg propagation into the subcommand's match scope — and clap does not merge multi-value global args across levels (long-standing behavior; see clap-rs/clap#3959): the scope that "wins" supplies the whole list. That's exactly your observation — one-cafterapp-serverand the pre-subcommand overrides vanish wholesale, with no warning, because from clap's perspective nothing was dropped.Fix. Bring
app-serverin line with the existing pattern:#[command(flatten)] config_overrides: CliConfigOverridestoAppServerCommand,prepend_config_flags(&mut app_server_cli.config_overrides, root_config_overrides)like the other arms, and pass the merged set onward.That preserves the documented precedence (root flags weaker than post-subcommand flags) and makes both your invocations equivalent.
Worth a quick audit while fixing: any other subcommand that accepts trailing
-cwithout its own flattenedCliConfigOverrides+ prepend call has the same silent-drop shape — the pattern being convention rather than enforced makes this easy to reintroduce. A small regression test that runs the parser with-c a=1 <subcmd> -c b=2and asserts both keys survive, parameterized over subcommands, would lock the convention in.