codex-cli app-server (Linux) leaks chrome-devtools-mcp child trees on JSON-RPC transport close
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):
- #17574 — Codex App on macOS / subagent spawn pattern
- #17832 — Playwright MCP regression after #16895
- #14548 — Codex spawning too many MCP instances
- #21008 — Codex Desktop launchd leak
- #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):
```
/usr/local/bin/codex exec -p advisory -C /tmp -s read-only --skip-git-repo-check --ephemeral --json --output-last-message …
app-server` via the socket.
The bridge spawns a fresh codex worker which talks to the long-lived
- 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
- #17574 (Codex App / macOS / subagent): same defect class, different platform.
- #21008 (Codex Desktop / launchd): same defect class on macOS Desktop.
- #16895 (the prior Playwright MCP fix) and #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
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗