Concurrent `codex mcp remove` silently loses updates in ~/.codex/config.toml

Open 💬 1 comment Opened Mar 27, 2026 by vladimir-mikhailov

What version of Codex CLI is running?

codex-cli 0.116.0-alpha.10

What subscription do you have?

ChatGPT account via Cursor-hosted Codex extension login. The exact plan is not surfaced by codex --version / codex login on this machine.

Which model were you using?

gpt-5.4

What platform is your computer?

Darwin 25.3.0 arm64 arm

What terminal emulator and version are you using (if applicable)?

Codex CLI binary bundled inside the Cursor extension (openai.chatgpt-26.318.11754-darwin-arm64), launched from a local shell on macOS.

What issue are you seeing?

codex mcp add / codex mcp remove appear to rewrite the full ~/.codex/config.toml without any cross-process locking or compare-and-swap protection.

When several codex mcp remove commands run in parallel against the same HOME, each process reports success (Removed global MCP server '...'), but the final config.toml keeps some of the supposedly removed MCP blocks.

This makes batch cleanup unsafe because the CLI reports success even though the final config is only partially updated.

Initially this looked like an OAuth-specific problem because I first observed it while removing notion and sentry, but after isolating the repro I could reproduce the same lost-update behavior with mixed MCP entries. Running the same removals sequentially works correctly.

What steps can reproduce the bug?

Use a disposable HOME so this does not touch your real Codex config:

tmp=$(mktemp -d)
mkdir -p "$tmp/.codex"
cat > "$tmp/.codex/config.toml" <<'CONFIG'
model = "gpt-5.4"
model_reasoning_effort = "xhigh"

[mcp_servers.context7]
command = "npx"
args = ["-y", "@upstash/context7-mcp@latest"]

[mcp_servers.figma]
url = "https://mcp.figma.com/mcp"

[mcp_servers.linear]
url = "https://mcp.linear.app/mcp"

[mcp_servers.notion]
url = "https://mcp.notion.com/mcp"

[mcp_servers.playwright]
command = "npx"
args = ["-y", "@playwright/mcp@latest"]

[mcp_servers.sentry]
url = "https://mcp.sentry.dev/mcp"

[features]
multi_agent = true
CONFIG

HOME="$tmp" codex mcp remove figma &
HOME="$tmp" codex mcp remove notion &
HOME="$tmp" codex mcp remove playwright &
HOME="$tmp" codex mcp remove sentry &
wait

cat "$tmp/.codex/config.toml"

Observed result on my machine:

  • every process prints Removed global MCP server '...'
  • the final config.toml still contains some of figma, notion, playwright, sentry
  • the exact survivors depend on write timing, which looks like last-writer-wins behavior

Control case:

  • if I run the same codex mcp remove ... commands sequentially, all target MCP entries are removed correctly

What is the expected behavior?

One of these should happen:

  1. Codex serializes config writes so concurrent mcp add/remove operations are safe.
  2. Codex detects that config.toml changed since read and retries / fails explicitly.
  3. Codex at minimum returns a non-zero exit code instead of reporting success when another process has overwritten its change.

The CLI should not silently lose some removals while still printing a success message for every command.

Additional information

  • I found the issue while cleaning up MCP servers from a shared Codex profile.
  • Sequential mutation is a viable workaround, but it is easy to assume parallel cleanup is safe because the CLI reports success.
  • I did not need OAuth login to reproduce the core bug once I switched to the disposable-HOME repro above.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗