codex mcp-server: `-c` config overrides are accepted but never applied to sessions

Resolved 💬 2 comments Opened Jul 29, 2026 by aburkard Closed Aug 26, 2026
💡 Likely answer: A maintainer (etraut-openai, contributor) responded on this thread — see the highlighted reply below.

Codex CLI version: 0.145.0
Subscription: ChatGPT Pro
Model: gpt-5.6-sol
Platform: macOS 26.5.0, arm64
Terminal: ghostty 1.3.1

What issue are you seeing?

codex mcp-server advertises the same -c, --config <key=value> flag as the other
subcommands, and it parses without complaint, but the override never reaches the sessions
the server creates. Those sessions fall back to whatever is in ~/.codex/config.toml.
Nothing warns you, so a config that didn't apply looks identical to one that did.

I ran into this setting approvals_reviewer for an MCP client that wraps
codex mcp-server. Starting the server with -c approvals_reviewer=user left sessions on
guardian_subagent from my config.toml. Passing the same setting through the codex
tool's per-call config argument works, so only the CLI flag is affected.

model is the easiest way to see it, since session_configured reports the model it
resolved.

What steps can reproduce the bug?

With model = "gpt-5.6-sol" in ~/.codex/config.toml:

  1. Start codex mcp-server -c model="gpt-5.2"
  2. Send initialize, then notifications/initialized
  3. Call the codex tool with any prompt, e.g.

{"cwd": "/tmp", "sandbox": "read-only", "approval-policy": "never", "prompt": "Reply with only: OK"}

  1. Read model from the session_configured event

Observed model=gpt-5.6-sol, expected gpt-5.2. Running it again with no -c gives the
same value, which is how I convinced myself the flag is doing nothing at all rather than
being partially applied.

<details>
<summary>Script that runs both cases and prints them side by side</summary>

"""Compare the model resolved by `codex mcp-server` with and without -c."""
import json, subprocess, threading, time

def run(extra_args, label):
    proc = subprocess.Popen(["codex", "mcp-server", *extra_args],
        stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
        text=True, bufsize=1, cwd="/tmp")
    done, seen = {}, {}

    def send(o):
        proc.stdin.write(json.dumps(o) + "\n"); proc.stdin.flush()

    def reader():
        for line in proc.stdout:
            try:
                m = json.loads(line)
            except json.JSONDecodeError:
                continue
            if m.get("method") == "codex/event":
                msg = (m.get("params") or {}).get("msg") or {}
                if msg.get("type") == "session_configured":
                    seen["model"] = msg.get("model")
            elif m.get("id") is not None and ("result" in m or "error" in m):
                done[m["id"]] = m

    threading.Thread(target=reader, daemon=True).start()
    send({"jsonrpc": "2.0", "id": 1, "method": "initialize",
          "params": {"protocolVersion": "2025-06-18", "capabilities": {},
                     "clientInfo": {"name": "repro", "version": "1.0"}}})
    t = time.monotonic() + 30
    while 1 not in done and time.monotonic() < t:
        time.sleep(0.2)
    send({"jsonrpc": "2.0", "method": "notifications/initialized"})
    send({"jsonrpc": "2.0", "id": 2, "method": "tools/call",
          "params": {"name": "codex", "arguments": {
              "cwd": "/tmp", "sandbox": "read-only", "approval-policy": "never",
              "prompt": "Reply with only: OK"}}})
    deadline = time.monotonic() + 180
    while 2 not in done and time.monotonic() < deadline:
        time.sleep(0.25)
    proc.kill()
    print(f"  {label:<42} model={seen.get('model')}")

run([], "no -c flag (config.toml default)")
run(["-c", 'model="gpt-5.2"'], '-c model="gpt-5.2"')

Output:

  no -c flag (config.toml default)           model=gpt-5.6-sol
  -c model="gpt-5.2"                         model=gpt-5.6-sol

</details>

What is the expected behavior?

Either the override applies to the sessions the server creates, or codex mcp-server
rejects or warns about -c so it's obvious it had no effect. The silent success is the
part that cost me time.

Additional information

codex-rs/mcp-server/src/lib.rs does parse the overrides and build a Config from them,
so this looks like a propagation gap rather than intended behavior:

let cli_kv_overrides = cli_config_overrides.parse_overrides().map_err(|e| { ... })?;
let config = ConfigBuilder::default()
    .cli_overrides(cli_kv_overrides)
    .strict_config(strict_config)
    .build()
    .await

As far as I can tell that config is used for the server's own setup (otel, state db,
EnvironmentManager, codex_home, installation id) and isn't what the tool-call sessions
resolve from. I didn't trace where it gets dropped.

Closest existing issues I could find, both different: #35780 is about -c mis-splitting
quoted keys containing periods, and #13243 is about --help formatting for --config.

View original on GitHub ↗

2 Comments

Pybsama · 1 month ago

I traced this to the per-session config rebuild in CodexToolCallParam: run_main parses the root -c values and uses them for the MCP server's base Config, but each codex tool call creates a fresh ConfigBuilder from only the call's config and typed fields.

I have a focused local patch that retains the parsed startup override list in MessageProcessor and merges it in this order for ordinary user-controlled keys:

config.toml < codex mcp-server -c < tool-call config < typed tool fields

Managed configuration and requirements retain their existing authority. The patch preserves the public CodexToolCallParam::into_config(Arg0DispatchPaths) signature and does not change --strict-config, schemas, dependencies, codex-core, or codex-reply.

The regression test launches the real top-level codex mcp-server and checks three session_configured events: startup overrides, request-level overrides, and a typed model override. It isolates CODEX_HOME, CODEX_SQLITE_HOME, proxies, and host-managed config; a missing test-only provider key stops submission before model transport/socket I/O.

The test failed before the production change with config-model instead of startup-model. Local validation now passes:

  • just test -p codex-mcp-server — 15/15
  • just test -p codex-cli --test mcp_server — 1/1
  • just test -p codex-cli — 339/339
  • cargo clippy -p codex-mcp-server --tests --profile release -- -D warnings
  • just fix -p codex-mcp-server
  • just fix -p codex-cli
  • just fmt

I have kept the branch local and have not opened a PR because external code contributions are invitation-only. If this approach aligns with the intended fix, would a maintainer be willing to invite a PR? I can submit the tested patch promptly.

etraut-openai contributor · 2 days ago

Note: We've announced that the mcp-server is deprecated and will be removed in a future release. We recommend using the app server interface instead.