Ctrl+C can become non-functional during /review and MCP startup (startup/shutdown deadlock)
Summary
Ctrl+C can become non-functional in startup-heavy flows (notably /review and MCP initialization), leaving Codex running in a state where keyboard interrupts do not exit the app.
In affected runs, repeated Ctrl+C does not recover. The only reliable escape is external process termination (closing terminal/tab, killing tmux pane, kill, etc.).
User impact
- Breaks the primary safety/escape interaction in the CLI.
- Can strand users in long-running or hung startup paths.
- Makes
/reviewand initial MCP setup feel deadlocked even when user requests cancellation.
Reproduction scenarios
Scenario A: /review during startup pressure
- Start Codex in an environment where startup is slow/hanging (for example MCP server startup delays).
- Trigger
/review. - Press
Ctrl+Cone or more times.
Observed in failing cases: app does not exit, and keyboard interrupt appears ignored.
Scenario B: MCP initialization with unresolved client future
- Start with MCP servers where one startup future does not resolve promptly.
- During initialization, press
Ctrl+C.
Observed in failing cases: shutdown request can be starved; process stays alive until externally killed.
Root cause analysis
This was a compound bug with multiple contributing paths:
- Startup op starvation in TUI agent startup path
- Before fix,
spawn_agentawaitedstart_threadbefore consuming queued ops. - If startup hung,
Op::Shutdown(triggered by quit flow /Ctrl+C) could not be handled.
- Unbounded or effectively unbounded MCP client waits in startup-sensitive paths
- Several MCP manager call sites awaited shared client futures directly.
- If client creation/startup stuck, startup progress and related status flows could stall.
- Shutdown-first exit lacked hard escalation path
- Repeated quit requests could continue routing through
ShutdownFirstwithout a guaranteed fallback to immediate exit when shutdown completion was stuck.
- Review sub-agent startup was not cancellation-aware while spawning
/reviewpath called sub-agent spawn directly; cancellation token was not raced for the spawn await.
Implemented fix
I prepared and validated a two-commit fix on branch mingley/ctrlc-interrupt-fix:
e403fd6c7dd5aa277ad6d6f6b6c1323b7a1d102a3d8dccdea337b2b5ff44db929a50d57a1ecdf565
1) Keep startup interruptible in TUI (codex-rs/tui/src/chatwidget/agent.rs)
- While startup is pending, consume ops from the op channel.
- If
Op::Shutdownarrives pre-startup, emitAppEvent::Exit(ExitMode::Immediate)and return. - Buffer non-shutdown startup ops and flush after startup completes.
2) Bound MCP startup waits across manager call paths (codex-rs/core/src/mcp_connection_manager.rs)
- Added startup timeout-aware client helper and applied it to startup-sensitive operations.
- Added timeout-based behavior for startup-failure checks and aggregate listing paths.
- Updated additional remaining raw startup waits (
notify_sandbox_state_change, startup join task) to use bounded waits.
3) Add shutdown escalation fallback (codex-rs/tui/src/app.rs)
- Added explicit escalation behavior: repeated
ShutdownFirstquit request exits immediately. - This preserves graceful shutdown attempt first, but guarantees a hard user escape hatch on repeated request.
4) Make review sub-agent spawn cancellation-aware (codex-rs/core/src/codex_delegate.rs)
- Wrapped
Codex::spawn(...)with cancellation (or_cancel) in review/delegate startup path. - Prevents
/reviewfrom hanging uninterruptibly during spawn waits.
Test coverage and validation
Added/validated tests relevant to these regressions:
mcp_connection_manager::tests::required_startup_failures_times_out_pending_servermcp_connection_manager::tests::list_all_tools_skips_pending_server_after_timeoutapp::tests::shutdown_first_exit_escalates_on_second_requestchatwidget::tests::ctrl_c_shutdown_works_with_caps_lock
Validation commands run:
cargo fmtcargo clippy --all-targets --all-features -- -D warningscargo test -p codex-core required_startup_failures_times_out_pending_servercargo test -p codex-core list_all_tools_skips_pending_server_after_timeoutcargo test -p codex-tui shutdown_first_exit_escalates_on_second_requestcargo test -p codex-tui ctrl_c_shutdown_works_with_caps_lockcargo test -p codex-tuicargo test(workspace)
Workspace test note:
cargo testfails in this environment only on pre-existing core tests unrelated to this fix:shell::tests::detects_bashseatbelt::tests::create_seatbelt_args_with_read_only_git_pointer_fileseatbelt::tests::create_seatbelt_args_with_read_only_git_and_codex_subpaths
Why this contribution should be accepted
- High user impact: restores reliability of
Ctrl+C, the primary fail-safe control. - Clear causal diagnosis: fix addresses each independent blocking path, not just symptoms.
- Surgical scope: targeted changes in affected startup/shutdown paths only.
- Regression coverage: added focused tests for timeout and shutdown escalation behavior.
- Low behavior risk: normal successful flow remains unchanged; only failure/cancellation paths are hardened.
Request for contribution path
External PRs are currently invitation-only. Please consider one of:
- Allowing me to submit this as a PR directly, or
- Cherry-picking the two commits above.
I can also split the commits further if maintainers want each root-cause fix isolated.
6 Comments
Opened PR with the proposed fix set: https://github.com/openai/codex/pull/11268
This PR includes the startup interruptibility changes, bounded MCP startup waits, review spawn cancellation handling, and shutdown escalation fallback discussed in this report.
Update: PR https://github.com/openai/codex/pull/11268 was closed by maintainer comment on 2026-02-10 due current contribution policy (invitation-only code contributions):
https://github.com/openai/codex/pull/11268#issuecomment-3875185454
To keep this issue actionable, the tested fix set remains available in that PR and in these commits:
e403fd6c7dd5aa277ad6d6f6b6c1323b7a1d102a3d8dccdea337b2b5ff44db929a50d57a1ecdf565What the fix covers:
Op::Shutdownhandled during pendingstart_thread)Validation run for these changes:
cargo fmtcargo clippy --all-targets --all-features -- -D warningscargo test -p codex-tuiIf maintainers want, I can provide this as split commits by root cause to make internal cherry-picking/review easier.
Reproduced on v0.122.0, macOS 26.4 arm64, 9 MCP servers configured.
sample <codex_pid> 3 output:
26/26 tokio-runtime-worker threads + main thread all parked in _pthread_cond_wait / __psynch_cvwait.
Zero threads doing I/O.
/healthz and /readyz on the app-server WS port don’t respond.
TUI stuck on "Starting MCP servers (8/9): <name>" for 10+ min.
Ctrl-C inert.
Child MCP processes themselves are healthy, isolated initialize handshake responds in ~500ms. They’re waiting on stdin that never arrives because the parent is deadlocked. Plus a reference to mcp_connection_manager.rs pre-spawn loop + emit_update(...).await on bounded channel as the suspected culprit.
I can optionally attach the full sample file as a gist if they want the raw stacks.
hit this same pattern across three stdio MCP setups on macOS last quarter. the bounded channel emit_update during startup is the immediate trigger, but the deeper issue is the initialize handshake has no watchdog separate from the RPC timeout. if a child binary spawns but stalls before reading initialize, the parent blocks on the pipe write because macOS pipe buffers default to 16KB vs linux 64KB. i wrap every spawn in a 5s initialize deadline plus a 2s 'first bytes out' watchdog on stdout, kill on either. double-Ctrl-C hard-exit is the right call, matches how reliable daemon wrappers handle it.
Thanks for the bug report. This will be fixed in the next release.
Looks like a regression here,
ctrl+cconsistently fails to kill the process when runningcodex review.OS:
MacOS Tahoe Version 26.6.1CLI:
codex-cli 0.146.0