Repository navigation
codex-cli app-server (Linux) leaks chrome-devtools-mcp child trees on JSON-RPC transport close #24347
Description
What version of the Codex CLI are you using?
codex-cli 0.132.0 (@openai/codex@0.132.0, npm-installed at /usr/lib/node_modules/@openai/codex/)
What subscription do you have?
Pro
What platform is your computer?
Linux 6.8.0-117-generic x86_64 (Ubuntu 24.04, Hostinger VPS, 32 GB RAM, 8 vCPU)
What issue are you seeing?
codex app-server --listen unix://… does not reap chrome-devtools-mcp (stdio MCP) child process trees when the JSON-RPC transport closes. Each orphan tree is npm exec → sh -c → chrome-devtools-mcp ≈ 270 MB RSS. They accumulate indefinitely until OOM or until external intervention.
Related but distinct issues (different surface, same defect class):
- Subagents leak stdio MCP helper trees in Codex App; xcodebuildmcp and chrome-devtools-mcp accumulate indefinitely #17574 — Codex App on macOS / subagent spawn pattern
- Regression: Playwright MCP stdio processes still leak after #16895 fix — 213 orphaned pairs, 13.6 GB RSS #17832 — Playwright MCP regression after Stdio MCP child processes are not cleaned up and accumulate indefinitely #16895
- Codex spawning too many mcp instances and never kills them, memory is getting overwhelmed #14548 — Codex spawning too many MCP instances
- Codex desktop leaks orphaned MCP helper processes under launchd #21008 — Codex Desktop launchd leak
- MCP servers eagerly start per session, causing headed browser MCP processes to accumulate #21984 — Eager MCP start per session
This report is specifically about codex-cli's app-server mode on Linux under the claude-bridge.sh peer-agent pattern (Claude Code invoking codex via Unix-socket JSON-RPC), which none of the above cover.
Observed accumulation
Steady-state on my VPS over a ~3-hour window:
codex app-server PID: 1754 (single instance, uptime ~8 h)
chrome-devtools-mcp wrappers under PID 1754: 4
ages: 11974s, 1236s, 1081s, 1056s (only the 11974s could plausibly be "active";
the other 3 were spawned during one-shot
`codex exec` runs via codex-cli's bridge mode)
combined RSS: ~1.1 GB
Process tree (one of the four):
1115984 ppid=1754 npm exec chrome-devtools-mcp@latest --no-usage-statistics --no-performance-crux --headless
1116129 ppid=1115984 sh -c chrome-devtools-mcp --no-usage-statistics --no-performance-crux --headless
1116130 ppid=1116129 chrome-devtools-mcp
The wrapper PID's parent is the codex app-server daemon. The MCP server never received a graceful close-on-shutdown signal — once the originating JSON-RPC channel closed (single tool call completed), the wrapper + child remained alive.
Steps to reproduce (deterministic)
- Start codex
app-server:codex app-server --listen unix:///tmp/codex-app-server.sock - From another agent (e.g. Claude Code) over the same socket, fire a
tools/callfor achrome-devtools-mcptool method (we use the bridge wrapperclaude-bridge.sh):The bridge spawns a fresh codex worker which talks to the long-lived/usr/local/bin/codex exec -p advisory -C /tmp -s read-only --skip-git-repo-check --ephemeral --json --output-last-message …app-servervia the socket. - Inspect
ps --ppid <app-server-pid>— thenpm exec chrome-devtools-mcpwrapper is alive. - The bridge worker exits, the JSON-RPC channel for the chrome MCP closes from the client side, BUT the wrapper remains alive under
app-server. - Repeat. Each invocation adds one orphan.
Expected behavior
When the JSON-RPC transport to a stdio MCP child closes (or when its consuming session shuts down), app-server should:
- Send SIGTERM to the wrapper's process group.
- Wait a bounded grace (e.g. 5 s).
- SIGKILL the process group's surviving descendants (collected before SIGTERM, so reparented chromium survives only briefly).
Actual behavior
Wrapper + descendants stay alive indefinitely. Memory leak scales linearly with tool-call count.
Workaround (containment, not a fix)
I deployed a per-host cron reaper that:
- snapshots
codex app-serverPIDs +/proc/<pid>/stat[22]start times, - enumerates
npm exec chrome-devtools-mcpwrappers parented to the snapshot set, - keeps the youngest per app-server plus any wrapper younger than 10 min (grace for in-flight calls),
- SIGTERMs the rest deepest-first; 5 s; SIGKILLs survivors from the SAVED descendant tree.
Source: happy to paste, but it should not be the fix — app-server knows the exact moment a transport closes; a host-side reaper is guessing.
Suggested fix locations
(I have not read the codex-cli source yet, but the surface area implied by the symptom:)
- The MCP transport-close handler for stdio servers should track child PIDs and tear the process group down explicitly.
node:child_processspawn({ detached: false })puts children in the parent's PG;process.kill(-pgid, 'SIGTERM')works. Fordetached: true, the spawn site must storepidand callkill(-pid, …)on transport close. - A short post-
SIGTERMgrace plusSIGKILLcovers chromium's slow shutdown. - Belt-and-suspenders: on
app-servershutdown (SIGTERM to itself), iterate all known MCP child PGs and kill them, since a normalexit()of the daemon does not cascade through.
Cross-references
- Subagents leak stdio MCP helper trees in Codex App; xcodebuildmcp and chrome-devtools-mcp accumulate indefinitely #17574 (Codex App / macOS / subagent): same defect class, different platform.
- Codex desktop leaks orphaned MCP helper processes under launchd #21008 (Codex Desktop / launchd): same defect class on macOS Desktop.
- Stdio MCP child processes are not cleaned up and accumulate indefinitely #16895 (the prior Playwright MCP fix) and Regression: Playwright MCP stdio processes still leak after #16895 fix — 213 orphaned pairs, 13.6 GB RSS #17832 (regression after it): suggests a shared lifecycle bug that the Playwright-specific fix didn't generalize to other stdio MCPs.
Environment
- OS: Ubuntu 24.04, kernel 6.8.0-117-generic
- Node: bundled with codex-cli (
@openai/codex-linux-x64) chrome-devtools-mcpversion: latest (npm exec chrome-devtools-mcp@latest)- Invocation:
codex app-server --listen unix://+ peer-agent bridge over the socket - Concurrent CCD (Claude Code) sessions on the same host (7 active) — their own MCP children are correctly scoped and do not leak, only the codex
app-serverside does
Activity
github-actions commentedon May 24, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
jshaofa-ui commentedon May 24, 2026
Technical Proposal: Fix MCP Child Process Leak on Transport Close
Service Match: A. MCP 集成服务
Timeline: 2-3 days
Competition: 1 comment
Problem Analysis
When JSON-RPC transport closes, the app-server fails to clean up child processes spawned by chrome-devtools-mcp, causing resource leaks and port conflicts.
Proposed Fix
Deliverables
yui-stingray commentedon May 24, 2026
One concrete layer to inspect is the stdio MCP launcher rather than only the app-server connection gate. On current
main(9f42c89):codex-rs/app-server/src/lib.rs:902-914handlesTransportEvent::ConnectionClosedby removing the connection state, notifying outbound control, and callingprocessor.connection_closed(...); this is not itself the direct MCP child cleanup site.codex-rs/app-server/src/message_processor.rs:709-723: it shuts down the per-connection RPC gate and clears outgoing/fs/exec/thread bookkeeping. The thread side (codex-rs/app-server/src/request_processors/thread_processor.rs:2396-2408) removes the connection subscription and only finalizes stale bookkeeping if the thread was already removed; I do not see it directly submitting a thread/session shutdown there.codex-rs/codex-mcp/src/rmcp_client.rs:575-590, then the shared transport close path callsself.process.terminate().awaitincodex-rs/rmcp-client/src/stdio_server_launcher.rs:128-133.codex-rs/rmcp-client/src/stdio_server_launcher.rs:262-263), and the process-handleDroppath also terminates if close did not already run (:397-405). The executor-backed transport has a similar close-time terminate atcodex-rs/rmcp-client/src/executor_process_transport.rs:159-162.codex-rs/rmcp-client/tests/process_group_cleanup.rs:98-130and:133+checksdrop(client)/client.shutdown(), but I do not see a regression test for the app-server JSON-RPC connection-close path described in this report.So if this reproduces with
chrome-devtools-mcp, the gap to verify may be whether app-server JSON-RPC connection close actually reaches theRmcpClient/stdio transport close or drop path for the session that owns the MCP server. The process-group cleanup primitive appears to exist; the uncertain part is whether this app-server close path drives it.w2jmoe commentedon May 27, 2026
This is a really good write-up 👀
The interesting part is that the leak is not coming from model execution itself, but from transport/session lifecycle boundaries.
Once the JSON-RPC channel closes, the runtime seems to lose ownership of the MCP child tree, so the process survives outside the original execution context.
That’s a pretty nasty failure mode for long-running agent systems because the leak scales with tool-call count, not just session duration.
The “youngest-per-server + grace window” reaper workaround is also clever 😊
It basically recreates a missing lifecycle manager externally.
Feels like MCP runtimes may eventually need explicit “capability ownership” tracking:
which session spawned which process tree, who is responsible for teardown, and what happens during transport interruption / reconnect / partial failure.
Otherwise these orphaned execution surfaces become surprisingly hard to reason about once multiple agents/tools/sessions coexist.