Codex.app GUI: MCP child processes not reaped after task completion — 1300+ zombies, 37GB memory leak
Codex.app GUI: MCP child processes not reaped after task completion — 1300+ zombies, 37GB memory leak
What version of Codex CLI is running?
codex-cli 0.98.0 (Codex.app GUI wrapper)
What subscription do you have?
ChatGPT Pro
Which model were you using?
o3 (via Codex.app GUI)
What platform is your computer?
macOS 26.3 (25D125), Apple M4 Max, 64GB RAM, arm64
What issue are you seeing?
Codex.app spawns codex exec --full-auto worker processes for each worktree but never reaps them or their MCP child processes when tasks complete. Over multiple audit sessions, this accumulated 1,319 zombie Codex processes and 1,537 node processes consuming \~37GB RSS + 40GB swap on a 64GB machine, causing severe system-wide slowdown.
Root cause analysis
Each Codex.app task creates:
- 1×
codex exec --full-auto --skip-git-repo-check --json -(CLI worker) - 4×
oh-my-codexMCP servers (state, memory, trace, code-intel) - 1× Serena MCP server (via
uv tool uvx) - 1×
uvprocess for Serena
≈ 7 processes per task. None are killed when the task completes.
With 12 git worktrees created by Codex.app (detached HEAD, pointing at old commits), this produced:
- 12 worktrees × \~110 child processes = \~1,320 sleeping processes
- Combined RSS: \~37GB (mostly npm/node/uv MCP servers)
- Swap usage: 39.4GB (on 64GB physical RAM)
When Codex.app (PID 19947) was killed, 2 additional codex exec workers (launched Friday and Saturday) were reparented to launchd (PPID=1), confirming they were never properly cleaned up.
Process tree snapshot (before cleanup)
Codex.app (PID 19947, 279MB)
├── codex exec --full-auto (PID 19970, 123MB)
│ └── 880× node/npm/uv children (27GB total)
│ ├── npm (236MB each, ×2)
│ ├── npm (140MB each, ×4)
│ ├── uv (91MB each, ×2)
│ └── node (77MB each, ×dozens)
├── Codex Helper Renderer (PID 20018, 602MB)
└── Codex Helper (PID 19971, 101MB)
Orphaned (PPID=1, reparented from dead Codex.app sessions):
├── codex exec --full-auto (PID 59326, Fri 23:59)
│ ├── oh-my-codex/state-server.js
│ ├── oh-my-codex/memory-server.js
│ ├── oh-my-codex/trace-server.js
│ ├── oh-my-codex/code-intel-server.js
│ └── serena start-mcp-server
└── codex exec --full-auto (PID 59747, Sat 22:05)
└── (same MCP stack)
Memory impact
<!-- linear:table-colwidths:200,200,200,200 -->
| Metric | Before cleanup | After cleanup | Delta |
| -- | -- | -- | -- |
| Swap used | 39.4 GB | 8.0 GB | -31 GB |
| Codex processes | 1,319 | 0 | -100% |
| Node processes | 1,537 | 17 | -99% |
| Free pages | 82K | 2.3M | ×28 |
Worktree state at time of discovery
All 12 worktrees were detached HEAD on old commits (2+ phases behind current main), confirming tasks had completed long ago:
/Users/drg/.codex/worktrees/0585/medical-ai 627d44e (detached HEAD)
/Users/drg/.codex/worktrees/0b7b/medical-ai 627d44e (detached HEAD)
/Users/drg/.codex/worktrees/2a60/medical-ai 4d53e7d (detached HEAD)
... (12 total, all on 2 old commits)
Current HEAD: 71aefff — none of the worktrees were anywhere near current.
What steps can reproduce the bug?
- Use Codex.app GUI to run multiple tasks on a git repository (especially with MCP servers configured via oh-my-codex)
- Let tasks complete normally
- Wait a few hours / run more tasks
- Check process count:
ps aux | grep -c codex - Check memory:
ps aux | grep codex | awk '{sum+=$6} END {printf "%.0f MB\n", sum/1024}'
Accumulation is proportional to number of tasks × MCP servers configured.
What is the expected behavior?
When a Codex.app task completes:
- The
codex execworker process should be terminated - All MCP child processes (oh-my-codex servers, Serena, uv) should be killed via process group signal
- The git worktree should be cleaned up if the task is done (or at minimum, child processes should not persist)
What is the actual behavior?
- Worker processes and their entire MCP subtree remain alive indefinitely
- No cleanup occurs on task completion, app quit, or even app crash (reparented to launchd)
- Memory grows unboundedly with each task
Relationship to existing issues
- CLI-494 (fixed, Oct 2025):
start_kill()withoutwait()inexec.rs— same root cause pattern (missing process reaping), but fix only covered timeout/Ctrl+C in CLI, not GUI lifecycle - CLI-3017 (Jan 2026): CLI memory leak causing crashes
- [#12414](<https://github.com/openai/codex/issues/12414>) (Feb 2026, open): Unbounded memory growth when idle on v0.104.0
This issue is distinct because it specifically affects Codex.app GUI and its interaction with MCP server child processes — the GUI spawns long-lived codex exec workers that themselves spawn MCP servers, creating a deeper process tree that the existing cleanup logic does not handle.
Suggested fix
- Process group kill:
codex execworkers should usesetsid()/ process groups so all children (MCP servers) can be killed with a singlekillpg()on task completion - Worktree lifecycle: Codex.app should track active worktrees and clean up stale ones on startup (e.g., remove worktrees whose tasks are no longer active)
- Heartbeat/watchdog: MCP servers spawned by
codex execshould monitor their parent and self-terminate if the parent dies (check PPID periodically, or use a pipe/fd that breaks on parent death)
38 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
I put up a draft fix for this path: https://github.com/openai/codex/pull/12505
Scope is intentionally narrow and low-risk:
features.thread_listener_cleanupas an experimental flag (default off)I am currently seeing memory pressure in app usage with 5+ concurrent threads, so this is aimed at reducing retained in-memory thread/listener state without changing default behavior.
Given the updated contribution policy (invitation-only PRs), #12505 is currently a draft patch proposal to make discussion concrete. If this direction aligns with maintainer intent, would someone from the team be willing to explicitly invite that PR (or suggest a narrower scope)? I can provide additional repro/profiling details from macOS sessions where memory pressure climbs with 5+ concurrent threads.
Experiencing this on Linux / codex-cli 0.106.0
Reproducing the same issue. Here's a breakdown of the two distinct leak patterns I'm seeing:
Environment:
Crash:
failed to spawn thread: Os { code: 11, kind: WouldBlock, message: "Resource temporarily unavailable" }Two distinct leak patterns observed:
waitpid): A single long-running codex session accumulated 41 zombie<defunct>children — all from a Python stdio MCP server. The MCP server forks subprocesses for tool calls, but the codex parent never callswaitpid()to reap them.pstreeoutput:``
``codex,49983
|-(MainThread,105811) <defunct>
|-(MainThread,125138) <defunct>
|-(MainThread,131433) <defunct>
... (41 total zombies)
The combination of both leaks led to thread exhaustion and the crash. This appears to be the same root cause as #4726 (closed) and #6201 (closed) — neither of which fully resolved the lifecycle management for stdio MCP servers.
I can confirm a very similar issue on macOS, and I captured a full CLI process snapshot on March 11, 2026.
Environment
26.305.95014.3.1 (23D60)context7,stripe,playwrightWhat I observed
607total processes5760threads15G used,74M unused6608M compressor6342612swapins8519545swapoutsapp-serverPID:codex app-serverPID6030660306:7372were MCP launchers24 x npm exec @upstash/context7-mcp24 x npm exec @stripe/mcp --tools=all24 x npm exec @playwright/mcp@latest148processes totalThis does not look like a bounded “currently active work only” baseline. It looks like repeated MCP launcher accumulation over time under the same long-lived
codex app-server.Long-lived duplicated MCP launcher examples
8hold:npm exec @playwright/mcp@latestnpm exec @stripe/mcp --tools=allnpm exec @upstash/context7-mcp4hold:playwright/stripe/context73hold:1hold:50mold:20h+old:app-serverImportant distinction
#12333that duplicate MCP instances may be expected while subagents/threads are active.codex app-serverPID over many hoursThis feels closely related to:
#12491#11324#12976#10710Question for maintainers
#10710have covered this desktop app path as well?Thanks for taking a look. The snapshot seems to point to unbounded MCP process accumulation over time rather than short-lived fanout from currently active work, so hopefully it helps narrow the cleanup path.
This looks related to
#11324, but I wanted to add a narrower app-server angle that may help isolate root cause.I traced a reproducible case where the leaking MCP children are not orphaned from dead workers; instead they remain attached to the long-lived desktop
codex app-serverprocess after conversations are no longer active.The key detail from source review is that MCP servers are conversation-scoped and the intended unload path is
thread/unsubscribe. When the last subscriber unsubscribes, the app-server shuts down the thread and unloads it. However, on connection removal the current app-server code path retains zero-subscriber threads instead of unloading them, so theirMcpConnectionManagers never drop and the stdio MCP child processes stay alive.I posted the fuller write-up and evidence here:
#11324comment: https://github.com/openai/codex/issues/11324#issuecomment-4066938546So this may be the same family of leak as this issue, but specifically through desktop thread lifecycle / unload behavior rather than only worker reaping.
Same bug family on my Mac
@masvelio and @gabrielbryk this looks very close to what I hit. @RemarkRemedy also makes sense to me here, but i think there is one more phase after that app-server retention phase.
My setup
26.311.21342 (build 993)when this built upDarwin 25.3.0on Apple Silicon2.5 days18separate conversations/tasks in that windowcontext7,playwright,chrome-devtools,webflow mcp-remote, and one custom Python MCPWhat I saw
Each conversation was leaving its own MCP bundle under the long-lived
Codex.app -> codex app-servertree. Finished conversations were not going back to a small baseline.The extra part on my side
The main thing i did not see called out clearly yet is what happened on quit.
When I quit Codex from the Dock on March 17, 2026, most children died, but cleanup was only partial:
100to about10PID 1 / launchdMarch 9, 2026chrome_crashpad_handlerprocesses still alive fromMarch 9andMarch 14I also saw what looks racey. In one spawn generation, most siblings from that batch were gone, but one
twitterapi-docs-mcpsurvivor was still there by itself.Why I think this matters
#10710makes me think the stdio process-group cleanup on drop exists already. So maybe there are two gaps:One control point from the same machine: Claude Code via Superset.app only had about
11MCP processes from3active sessions, and those were cleaned up normally. So this did not look like a generic macOS "too many MCPs" problem.Small timing clarification on my earlier comment
One detail i want to tighten up from my earlier note here:
The orphan set i described was real right after
Cmd+QonMarch 17, 2026.At that moment, most Codex child processes died, but i still had a small
PID 1 / launchdresidue:Later, after I reopened Codex and closed it again, most of those leftovers finally disappeared.
So my main point is not "every orphan stayed forever".
It is: one quit cycle still left a small orphan set when the expected number should have been zero.
That still feels consistent with the two-part picture @gabrielbryk and @RemarkRemedy were circling around:
Fix implementation available
I hit this exact issue on a 16GB RAM Linux machine — running multiple Codex sessions with MCP servers (desktop-commander, context7, playwright, exa) accumulated 562 orphaned processes consuming 26GB of swap, triggering OOM kills and complete system freezes.
After investigating the source, I traced the root cause and implemented a fix. The branch is here:
👉 https://github.com/whatsgrowth/codex/tree/fix/mcp-process-cleanup (diff)
Root cause
McpConnectionManagerholds aHashMap<String, AsyncManagedClient>but has no explicit cleanup. Wrapped inArc<RwLock<>>insideSessionServices, theArc<Session>reference count may never reach zero (held by spawned tasks, event channels, etc.), soProcessGroupGuard::drop()never fires — and MCP server processes live forever.The low-level cleanup already works correctly:
ProcessGroupGuardinrmcp_client.rsproperly implementsDropwithkillpg(SIGTERM)→ 2s grace →killpg(SIGKILL). The problem is simply that it's never triggered.The fix (48 additions, 0 deletions, 2 files)
1.
core/src/mcp_connection_manager.rs— Addshutdown()method:2.
core/src/codex.rs— Call it inSession::shutdown():3.
core/src/mcp_connection_manager.rs— Safety netDrop:4.
core/src/codex.rs— Also clean up inrefresh_mcp_servers_inner:Verified
cargo check -p codex-core✅cargo test -p codex-core --lib -- shutdown— 7/7 pass ✅pgrepconfirmed all MCP processes were killed on exit ✅Could not open a PR directly (repo restricted to collaborators), so posting the fix here. Happy to rebase if needed.
Additional macOS desktop-app evidence for this issue on April 6, 2026.
I verified a live Codex.app session where this is happening with exactly one configured Playwright MCP server in
~/.codex/config.toml:Current live parent process:
/Applications/Codex.app/... codex app-server --analytics-default-enabled68203All leaking Playwright MCP descendants are parented to that single live app-server, not to old dead app instances.
Observed process tree totals:
55npm exec @playwright/mcp@latestwrapper processes55node .../playwright-mcpchild processes110total Playwright MCP descendantsObserved RSS:
241.6 MB106.8 MB348.4 MBImportant details:
This looks consistent with an app-side stdio MCP lifecycle/reaping bug rather than a user config problem or a one-off stale subtree.
Related issue that also seems relevant from the Playwright side: #14773.
Still reproducing on latest versions as of 2026-04-15.
Environment
0.119.0-alpha.28codexCLI0.120.0(invoked ascodex mcp-serverby an external client)Evidence —
ps -eo pid,etime,command:4
playwright-mcpchildren, oldest at 11h29m. 4codex mcp-serverparents, oldest at 11h34m. None correspond to an active session; they are leaked from sessions that ended hours ago./tmp/codex-playwright-mcp-cacheat 253 MB.Consistent with the 2026-04-06 Playwright-MCP comment above — still present on 0.119/0.120.
Still reproduces on v0.120.0 / Codex Desktop 26.409.20454. We observed 213 leaked
@playwright/mcpprocess pairs (426 processes, 13.6 GB RSS). Filed as #17832.PR #17223 (linked to #16895, merged April 10) fixed WebSocket client disconnect only. Subagent MCP teardown remains broken. See also #17574.
Root cause analysis with proposed fix: #17832 (comment)
https://github.com/openai/codex/issues/17832#issuecomment-4247026597
Update (2026-04-19): patch ready on branch
RedesignedRobot:codex:fix/mcp-subprocess-cleanup, rebased on currentmain,cargo check -p codex-core -p codex-mcpclean. 2 files, +23/-2, covers both theOp::Shutdownpath and thesubmission_loopchannel-close fallthrough. Awaiting invitation to open as PR per the invitation-only contribution policy.I get the memory leak on the Linux codex app port as well https://github.com/EvickaStudio/codex-app-linux-port
It spawns zombie MCP processes that eat 8gb of ram, my system regularly has to kill my browser tabs to keep the system from crashing. I think it's related to playwright as I have playwright as well.
To reproduce:
hit the same class of leak running a bunch of stdio MCP servers on macOS. the real culprit is process group handling, if you spawn the child without setsid/setpgid the parent can't send SIGTERM to the whole tree on exit, so npm/uv wrappers leak their node grandchildren. also node/npm wrappers don't forward SIGTERM to the real mcp binary by default, you need to exec the server directly or wrap with tini. and any stdio mcp needs an EOF-on-stdin watchdog inside the server so it self-exits if the parent dies without signaling. zombie reaping is table stakes but won't fix the reparented-to-launchd survivors, only pgid teardown does.
<img width="2481" height="1169" alt="Image" src="https://github.com/user-attachments/assets/49a0f852-aec3-4693-9f0a-8672ed531ce5" />
I got a new codex client so this is a different reproducibility process.
Note: Chromium is running on my system as a flatpak and codex isolates the codex renderer and MCP processes.
Personally I just disabled the oh my codex MCP servers to save memory and slow down the accumulation of zombie processes. As for every conversation it spawns more duplicate zombie MCP processes that never get freed. This is a bug where for each conversation it just creates MCP servers, but it should be shared between conversations on start.
I’m seeing what looks like the same Codex Desktop MCP lifecycle leak on macOS.
Environment:
/Applications/Codex.app/Contents/Resources/codex app-serverchrome-devtools-mcp,gitnexus mcp, Codexnode_replSymptoms:
Observed before cleanup:
chrome-devtools-mcp: about 368 matching processesgitnexus mcp: about 186 matching processesnode_repl: about 93 matching processeschrome-devtools-mcpalone was roughly 19 GB RSS in one snapshotgitnexus mcpwas roughly 8 GB RSS in one snapshotRelevant process pattern:
Manual cleanup results:
chrome-devtools-mcp,gitnexus mcp, andnode_replprocesses immediately reduced the process count.chrome-devtools-mcp: 0 initially, later restarted only when neededgitnexus mcp: 2node_repl: 1Expected behavior:
Actual behavior:
+1 confirming this is still active on Codex Desktop 26.422.x as of 2026-04-26, with a different MCP/feature manifestation worth naming explicitly: the Computer Use subsystem under
codex app-server. This reinforces what @bingyizhou6u flagged on 2026-04-25 with concrete pid-tree numbers.Environment
codex app-server --analytics-default-enabledparentProcess tree at peak
Codex Computer Use.app/Contents/SharedSupport/SkyComputerUseClient(4–16 MB RSS each)Codex.app/Contents/Resources/node_repl(4–7 MB RSS each)codex app-serverpid (not adopted by launchd, not detached)Why Activity Monitor shows 24+ GB for "Codex"
The individual RSS numbers above are tiny — the headline number is macOS's compressor pool, not resident memory.
vm_statshowed:\
\\\Free: 1.0 GB
Active: 14.2 GB
Inactive: 14.1 GB
Wired: 8.0 GB
Compressor: 25.7 GB ← here
Swap I/O: 66K in / 399K out
\
\Compressed pages get attributed back to whichever process owned them — so 100+ small worker working sets that got compressed under pressure and never freed showed up as a single 24.5 GB "Codex" attribution and triggered the macOS "Your system has run out of application memory" Force-Quit dialog.
Workaround (narrowly-targeted, does NOT touch
Codex.app,codex app-server, the active worker pool, or any other process)\
\\bash\pkill -f 'Codex Computer Use.app/Contents/SharedSupport/SkyComputerUseClient'
pkill -f 'Codex.app/Contents/Resources/node_repl'
\
\After running, free RAM moved 1.0 → 2.7 GB instantly; Codex respawned the correct ~4 workers from the active pool. Confirms these are leaked, not active.
Repro signal (one-liner check):
\
\\bash\ps -axo pid,command | grep -cE 'SkyComputerUseClient|node_repl'
\
\Healthy: 4–8. Leak active: 20+ and growing with uptime. Counts grow monotonically with Computer Use automation runs over time.
Suggested fix surface (consistent with #18881 and #17832): whatever invokes the Computer Use MCP tool needs to
child.wait()/ SIGTERM the spawned worker pair when the tool call resolves, the same lifecycle gap the broader MCP-reaping issue describes — Computer Use is just one more affected MCP host in the same pattern.Adding evidence on two more stdio MCPs in the same leak family on Codex Desktop.
Environment
26.422.30944on macOS Darwin 24.6.0, 10-core arm64, 24 GB RAMcodex app-server --analytics-default-enabledparent (PID 9529)playwright,lighthouse(local),supabase,herokuAffected MCPs (in addition to those already reported)
heroku(heroku mcp:start):bash heroku mcp:start→ 3 levels of bash wrappers →node run mcp:start→node heroku-mcp-server.mjsheroku plugins:inspect @heroku/plugin-aias part of plugin discovery on every startupheroku mcp:startbash wrappers parented to app-serverheroku-mcp-server.mjsnode processesnpm exec heroku@latest --replheroku plugins:inspect @heroku/plugin-aiprocesses — many with PPID=1 (parent bash chain died, signals not propagated; orphans reparented to launchd)setsid/killpg, Heroku's bash wrapper chain detachesplugins:inspectfrom the process group, so existing orphans wouldn't be cleaned up retroactively. New spawns under a process group would be fine.lighthouse(local stdio MCP):node /Users/steve/.codex/lighthouse-mcp-local/server.jsconfiglighthouse-mcp-local/server.jsprocesses + 7Codex.app/Contents/Resources/node_replprocesses, all parented to the live app-servernode_replappears paired with a lighthouse spawn — confirms whatbingyizhou6uandleojkwanreported aboutnode_replbeing part of the stdio MCP host patternSystem impact at peak
Workaround applied
enabled = falseon[mcp_servers.heroku]and[mcp_servers.lighthouse]in~/.codex/config.tomlpkill -f "lighthouse-mcp-local/server.js", same for heroku patternslaunchdjob) that detects duplicates of allowlisted MCP commands and kills all but the newest PID, until the upstream fix landsNotes for the suggested fix
The
m13vcomment on this issue covers the right surface. Adding two practical points from the heroku case:setsid+killpgin Codex would solve it cleanly — even with Heroku's leaky bash chain, every descendant inherits the process group at fork time.exec, an EOF-on-stdin watchdog inside the MCP server (perm13v) is the only thing that will reliably kill currently-running orphans whose parent died ungracefully.setsid/killpgis the right primary fix; the EOF watchdog is a robustness backstop.Happy to provide more raw
psoutput or repro logs if useful.I can reproduce a closely related variant of this on macOS, but specifically with the bundled Computer Use helper processes.
Environment:
26.422.30944(2080)26.3.1(25D2128)arm64computer-use@openai-bundled/computer-useplugin version1.0.758What I observed on April 28, 2026:
~/.codex/config.tomlhadplugins."computer-use@openai-bundled".enabled = true107instances of:./Codex Computer Use.app/Contents/SharedSupport/SkyComputerUseClient.app/Contents/MacOS/SkyComputerUseClient mcp/Applications/Codex.app/Contents/MacOS/Codex->
/Applications/Codex.app/Contents/Resources/codex app-server --analytics-default-enabled-> many
SkyComputerUseClient mcp~/.codex/plugins/cache/openai-bundled/computer-use/1.0.758/.../SkyComputerUseClientSkyComputerUseClientprocesses alone was about1.26 GB(average ~11.8 MBeach).2026-04-28 08:10, so these were not short-lived one-shot helpers.Important narrowing signal:
pkillagainstSkyComputerUseClient/SkyComputerUseService.pwd,date,sed ... package.json) and repeatedly re-checked process counts.0, and neitherSkyComputerUseClientnorSkyComputerUseServiceimmediately came back.That makes this look narrower than “all Codex activity leaks MCP children”.
It looks more like a lifecycle / teardown bug in the bundled
computer-useplugin path, or in howapp-servermanages that MCP/helper subtree after the helper has been activated.So this may be the same general cleanup class as this issue, but with a more specific reproduction target:
computer-use@openai-bundledSkyComputerUseClient mcpcodex app-serverafter prior usage instead of being reapedIf helpful, I can also provide a process-tree snapshot and exact command output showing the parent/child relationships.
Additional repro: Computer Use /
SkyComputerUseClientCPU fanoutI hit what looks like the same MCP lifecycle/leak family, but with a narrower Computer Use-specific trigger that may help isolate one upstream fix surface.
Environment
26.506.31421(CFBundleVersion2620)26.3.1(25D771280a)25.3.0,arm64computer-use@openai-bundledSymptom
Codex Desktop would spike into the 200-300% CPU range on normal prompt completions. CPU temps went over 220°F and the machine became close to unusable.
At peak I observed:
In one spike, the MCP/process fanout under the same
codex app-serveralso multiplied:The local Codex log DB was also writing heavily during the spike:
Failure mode 1:
SkyComputerUseClientinserted intoturn-endedMy
~/.codex/config.tomlhad been rewritten into this shape:That means ordinary prompt completions were routed through
SkyComputerUseClientbefore reaching the lightweight previous notifier.As a result, this path could fire on every normal response:
That seems like the wrong place for
SkyComputerUseClient. The user-facingnotifyhook is a high-frequency lifecycle hook for normal prompt completions. It should stay lightweight and bounded.Workaround for failure mode 1
I backed up
~/.codex/config.tomland restorednotifyto a lightweight wrapper:Then I fully restarted Codex Desktop to clear the already-spawned MCP/process tree.
After restart and another prompt:
Initially, this stopped the largest runaway. Computer Use could remain enabled for a short test. The critical fix for this first failure mode was removing
SkyComputerUseClientfrom theturn-endednotify wrapper path.Failure mode 2: Computer Use fanout even after
notifystays cleanAfter more testing, I reproduced a second related failure mode.
notifystayed clean:But with
computer-use@openai-bundledenabled, normal prompt/session activity still caused Computer Use-related children to respawn undercodex app-server:This points to a deeper Computer Use / MCP lifecycle problem, not a general claim that all user MCP servers are bad. The important narrowing signal is:
notifywrapper is one reproducible triggernotify,computer-use@openai-bundledcan still recreateSkyComputerUseClient/ MCP child-process fanoutSkyComputerUseClientfanout in my local containment testSo I think
SkyComputerUseClientshould only wake for explicit Computer Use actions. It should not be started by normal prompt completion or ordinary session lifecycle work.Suggested upstream fix
I think there are three related fixes here:
SkyComputerUseClientintonotify = ["...", "turn-ended"].turn-endedfires after normal prompt completions. It should not bootstrap Computer Use / MCP plumbing unless a Computer Use action was explicitly requested.If Codex sees:
``
toml
``notify = [".../SkyComputerUseClient", "turn-ended", "--previous-notify", "..."]
it should unwrap it back to the previous lightweight notifier, or at minimum warn the user and refuse to use
SkyComputerUseClientas a lifecycle notify handler.codex app-server.When
computer-use@openai-bundledis enabled,SkyComputerUseClient mcpinstances should converge back to a small stable baseline after tool/session work. If normal prompt activity can create repeated Computer Use client stacks, those children need explicit ownership, teardown, and process-group cleanup.This seems related to the broader MCP child lifecycle issue in this thread. The additional narrowing here is that Computer Use /
SkyComputerUseClientcan reproduce the runaway both through a badnotifywrapper and through plugin lifecycle/fanout even afternotifyis corrected.Quick user-side check
Anyone hitting this can check:
If
notifystarts withSkyComputerUseClient, try backing up the config, restoringnotifyto the previous lightweight notifier, and fully restarting Codex Desktop.If
notifyis clean butSkyComputerUseClient mcpkeeps multiplying, try disabling onlycomputer-use@openai-bundledas a containment test. I would not treat unrelated MCP servers as the first suspect unless their own process counts are also multiplying.The bug reported by the OP has been fixed. If you are seeing a similar behaviors in the latest version of the app, please open a new bug report.
Update after deeper local repro/debugging:
I want to correct the likely root-cause framing from my earlier comment. The Computer Use / MCP child-process fanout may have amplified the issue, but the strongest evidence now points to a huge text-like workspace artifact triggering the Electron/V8 hot path.
In the affected workspace, there was a generated embedded HTML file around 205MB. After moving that file out of the Codex-watched project root, the behavior improved substantially. The file was not an .xlsx issue; the Excel files in the project were small and Codex did not appear to have them open.
My current working theory is:
So I would reframe this as a possible Electron/workspace-ingestion bug involving very large text-like artifacts. MCP fanout may make the failure worse, but it may not be the original cause in this repro.
Suggested product-side guardrails:
This seems especially important for creative/game projects where generated HTML boards, embedded asset boards, image references, and exported visual docs can easily become hundreds of MB.
Additional sanitized macOS evidence matching this issue's MCP-child lifecycle failure.
Environment
Mac16,11, 24 GiB RAMObserved process tree after reboot
Within about 29 minutes of app/session recovery:
The earlier Jetsam snapshot recorded four active
codexworkers. Their ChatGPT/Codex coalition contained 1,201 active processes and about 48.89 GiB aggregate footprint.A normal AppleScript quit request returned successfully but the ChatGPT root and Codex worker remained alive. I then captured the exact ChatGPT tree and terminated only those 502 members, rechecking process name and start time before any forced signal. Without rebooting:
This machine also experienced a watchdog kernel panic after compressor segments reached 100% with 69 swapfiles. Another agent application contributed additional MCP trees, but the captured ChatGPT/Codex tree and the targeted before/after cleanup isolate a substantial Codex-side contribution.
The behavior appears proportional to active/restored workers multiplied by the global local-MCP set. Please consider process-group ownership/cleanup for each worker, parent-death signaling for MCP children, startup reconciliation of stale workers, and a regression test that verifies all MCP descendants exit after a task/app closes.
No username, hostname, IP address, absolute path, command argument, environment variable, credential, MCP server name, conversation content, session ID, UUID, PID, or raw core data is included.
A deterministic child-reaping mechanism was found in Codex CLI 0.144.1 and is still present in the current source. It is now tracked in #32219.
ProcessHandle::terminate() kills the child and then aborts wait_handle, while that wait task owns the child and is the only code that calls wait. This affects the PTY/pipe exec helper and is separate from the stdio MCP shutdown path addressed by #19753.
The deterministic reaping problem needs an owned process-tree lifecycle, not just best-effort cleanup at task end.
I would model every spawned Codex/MCP process with: owning task/session id, process group, child pids, startup receipt, heartbeat, terminal reason, reap attempt, and reap receipt. The critical invariant is that completion of the visible task must not be considered final until owned subprocess cleanup has either succeeded or produced a visible cleanup-failed state with enough ids for repair.
I maintain Better Agent (https://github.com/ofekron/better-agent), where detached runners and helper processes are tracked as durable owner state for crash/restart recovery. If useful, a star helps other desktop-agent builders find it.
Still reproducible on a current macOS desktop build, after #19753.
Environment
26.715.72359(CFBundleVersion 5718)codex-cli 0.145.0-alpha.30config.tomlI also checked the bundled CLI tag against the #19753 merge commit (
4e0cf94) through GitHub's compare API:rust-v0.145.0-alpha.30is 2,358 commits ahead and 0 behind, so the lifecycle fix is present in this build.Live process-tree snapshot
After approximately 90 minutes of app uptime:
codex app-serverincluded approximately:npm exec @playwright/mcp@latestwrappersnpm exec @azure/mcp@latestwrappersnpm exec chrome-devtools-mcp@latestwrappersnpm exec mcp-remotewrappersnode ./mcp/server.mjsprocessesThe duplicate process ages were staggered throughout the app session. These were not
PPID 1orphans: they remained attached to the live app-server process.This looks like a remaining lifecycle path where conversations/sessions stay loaded, so their per-session
McpConnectionManageris never shut down. In that case the explicit shutdown added by #19753 exists but is never reached. The observed multiplication also matches #20883's per-session MCP ownership description.A full app quit is the practical workaround reported in the related issues. I did not terminate this live instance during collection.
Feedback ID: 019f8c21-42cc-7993-9a3e-fe1cdde15482
Analysis (community)
Thanks for tracking this — attempting a residual-focused analysis aligned with docs/contributing.md (invitation-only: analysis in-thread, no unsolicited PR).
process-leak)Observed (from issue + public residual signal)
Codex.app GUI: MCP child processes not reaped after task completion — 1300+ zombies, 37GB memory leak ## What version of Codex CLI is running? codex-cli 0.98.0 (Codex.app GUI wrapper) ## What subscription do you have? ChatGPT Pro ## Which model were you using? o3 (via Codex.app GUI) ## What platform is your computer? macOS 26.3 (25D125), Apple M4 Max, 64GB RAM, arm64 ## What issue are you seeing? Codex.app spawns
codex exec --full-autoworker processes for each worktree but never reaps tRoot-cause hypothesis
MCP/Node helper processes retained after task completion (GUI path especially) accumulate CPU/RAM; aligns with desktop OOM reports.
High-level fix outline (not a PR)
Related
#12491 and desktop memory cluster (#33582, #32797).
Questions for maintainers
Happy to refine with more measurements or code pointers. No unsolicited PR from me.
Analysis (community)
Residual analysis for #12491 (GUI: MCP child processes not reaped) — invitation-only. Long-open process-lifecycle residual; distinct from Windows WMI/taskkill mega-threads (not pile-on of denylisted storms). No unsolicited PR.
Deterministic evidence (code)
Code (
5dd992acd3):codex-rs/mcp-server/tests/common/mcp_process.rstokio::process::Childwith.kill_on_drop(true)(≈L82)codex-rs/codex-mcpconnection manager (connection_manager/rmcp_clientre-exports) — lifecycle of external Node/stdio MCP servers must be joined on task end and session close.codex_mcp::connection_manager≈1745 TRACE rows undercodex-cli 0.146.0-alpha.6sessions on this host (indirect signal of frequent connect/disconnect churn).Root-cause hypothesis
Task completion / GUI session teardown does not always wait on MCP child process groups.
kill_on_dropin tests acknowledges cleanup is best-effort; production paths that spawn helpers without a guaranteed join accumulate orphans → RAM/CPU growth over long Desktop sessions (adjacent symptoms on #33582 / #32797, different platforms).High-level fix outline (not a PR)
waiton task end, thread close, and app-server shutdown.Questions for maintainers
codex-mcpconnection manager?No unsolicited PR from me.
Title:ChatGPT macOS app leaks zombie (defunct) child processes — exhausts
per-user process limit
App version: ChatGPT 26.730.61639 (Codex Framework 151.0.7922.71)
OS: macOS 14 (Darwin 23.2.0), Apple Silicon
Description:
The ChatGPT desktop app continuously accumulates <defunct> (zombie) child
processes without reaping them. The parent is the main ChatGPT process, and
the leak traces to the embedded Codex app-server / code-mode-host chain
(codex app-server → node_repl / codex-code-mode-host).
Rate: ~36 zombies/minute. After ~10 minutes of runtime the app held 400+
zombie processes.
Impact: On macOS the per-user process limit (kern.maxprocperuid) is 2666. At
this leak rate the app exhausts the process table in roughly an hour, after
which the entire system fails to fork new processes — every shell and app
throws fork failed: resource temporarily unavailable until ChatGPT is quit.
Quitting ChatGPT lets launchd reap all zombies instantly, confirming the app
is the sole cause.
Root cause (likely): child processes are fork()ed but the parent never calls
wait()/waitpid() to reap them.
Steps to reproduce:
code-mode/agent host spawns subprocesses).
periodically.
Expected: child processes are reaped; zombie count stays near zero.
Actual: zombies grow unbounded until the OS process limit is hit.
@yanxiaolong01's process chain narrows the ownership question I raised above:
codex app-server → node_repl / codex-code-mode-hostis the code-mode host spawn path, not the stdio MCP server path thatcodex-mcp's connection manager owns. ~36 zombies/min with the parent still alive means the children exit and are neverwait()ed — a missing reap on the host side, not a kill/teardown ordering bug.Two things that would make this actionable:
ps -o pid,ppid,stat,command -p <zombie-pid>for a handful of the defunct entries, plus the PPID chain. Whichever process is the direct parent owns thewait(), and that decides where the fix goes.node_replchildren explicitly, or does it rely on drop-time kill the way the test helper does (codex-rs/mcp-server/tests/common/mcp_process.rs,.kill_on_drop(true)≈L82, with the "not a guarantee" comment at ≈L39-41)?Fail-first test unchanged: spawn a code-mode cell, complete the task, assert zero defunct children under the app-server PID. No unsolicited PR from me.
Requested ps data (ChatGPT 26.730.61639 / Codex Framework 151.0.7922.71,
macOS 14.2 23C64, Apple Silicon)
Ran the scoped
psyou asked for. The direct parent of the zombies is NOTthe app-server — it's the top-level native host process.
Zombie sample (
ps -axo pid,ppid,stat,command):71372 70992 Z <defunct>
71442 70992 Z <defunct>
71445 70992 Z <defunct>
PPID chain:
<zombie 71372> Z --PPID--> 70992 --PPID--> 1 (launchd)
70992 S /Applications/ChatGPT.app/Contents/MacOS/ChatGPT # top-level
native host, PPID=1
71348 S /Applications/ChatGPT.app/Contents/Resources/codex -c
features.code_mode_host=true app-server --analytics-default-enabled #
PPID=70992
Zombie parent distribution (own zombies only):
949 70992 <- ChatGPT top-level native host (the leak)
19 1506 <- Google Chrome (unrelated 3rd party)
3 70835 <- GoogleUpdater (unrelated 3rd party)
Key finding that scopes the fix:
codex ... code_mode_host=true app-server(PID 71348) currently haszero child processes — it is not the direct parent of any defunct.
ChatGPTnative host (70992)**, whosePPID=1.wait()lives in the top-level host that actually spawns thecode-mode
nodechildren, not inside the app-server / MCPconnection-manager layer.
Rate: 70992-owned zombies went 972 → 981 in 15s (~36/min), parent stays in
state
S(alive) the whole time — confirms a pure missing-reap, not akill/teardown-ordering bug.
Windows evidence: the leaked children are NOT orphans, and lifetime CPU cannot identify them
Adding a Windows data point that differs from most of the thread in two ways that I think are actionable. Cross-references #22992 (duplicate MCP servers across conversations) and #30753 (duplicate pools after
RefreshMcpServers), which describe the same shape.Environment
26200.8973, 63.7 GiB RAMOpenAI.Codex_26.730.8199.0_x64codex.exe ... app-server --analytics-default-enabled, launched byChatGPT.execonfig.toml1. Every leaked process had a LIVE agent ancestor
I walked the full ancestry of all 605 MCP/language-server processes rather than checking the direct parent. All 605 resolved to a live
claude.exeorcodex.exeancestor. Zero were orphaned.A parent-is-dead check finds nothing here, which is why this can look like "no leak" to tooling that only looks one level up. The processes are attached to a live
app-serverand simply never serve again.Attribution by owning root:
| root | direct children | shape |
|---|--:|---|
|
claude.exe#79308 | 16 | exactly one of each server ||
claude.exe#43492 | 21 | ~one of each ||
claude.exe#38668 | 19 | ~one of each ||
codex.exe#81384 | 213 | 59 node, 42 graphify, 28 serena, 28 cocoindex, 13 vault-semantic, … |Totals:
codex#81384 owned 454 of the 605. The three Claude Code sessions owned 40–60 each.2. Spawn timeline: complete fleets, repeatedly, never reaped
33 processes = one full fleet of my 16 servers. Creation timestamps for
codex#81384's direct children:Roughly 13 fleets in about an hour. No previous fleet was terminated.
3. The discriminator: lifetime CPU is useless, windowed CPU is not
This is the part I have not seen stated in the thread, and it matters for anyone writing cleanup logic.
Several of my servers load an ML model at startup. Each
vault-semanticinstance burns 9–13 s of CPU importing torch and loading BGE-M3 before it ever listens. So a "has it done work?" test based on lifetime CPU classifies 7.5 GiB idle duplicates as busy and keeps them.Sampling CPU over a live window instead is blind to startup cost:
Example of the two figures disagreeing on the same process:
4. Cost
vmmemWSLand twopython.exeat 7.86 GiB eachTerminating all-but-newest per (root, server), restricted to processes idle over a live window, reclaimed 93.9 GiB and took commit from 95 % to 42 %. Nothing broke: the newest instance under each root is the one the session is actually using.
5. Suggestion
Whatever the eventual fix (#20883's project-scoped pool would remove the class entirely), a teardown heuristic that keys on liveness of the parent will not catch this, and one that keys on lifetime CPU will keep the most expensive offenders. Keying on "newest instance per (owning agent, server)" plus a windowed idleness check identified 499 of 669 processes correctly here with no false positives.
Happy to provide the raw per-process table or re-run any specific measurement.
Environment
Verification
Tested on the latest version (26.803.41515) with real-time
monitoring:
Conclusion
Issue Resolved: Version 26.803.41515 no longer exhibits
the zombie subprocess accumulation issue.
This thread conflates several different problems. That makes it difficult to tell which of these have been fixed and which still exist. I can say with high confidence that the original bug reported in this thread was fixed, which is why I initially closed the issue. It looks like it was reopened at some point because similar symptoms were reported. There is at least one other bug related to
ProcessHandle::terminate()that is a real bug, and I'll work on a fix for that one.If you are seeing other bugs that involve process leaks, please open a new issue and provide details so we can track it independently.
Thanks @etraut-openai — agreed this thread conflated several process-lifecycle failures.
My earlier residual notes were aimed at that same split (owned-process reaping /
ProcessHandle::terminate()vs the original Desktop zombie report). Standing down here; will only open a new issue if I can reproduce a distinct residual with a clean repro.The
ProcessHandle::terminate()issue will be addressed in the next release, so I'm going to close this. As I mentioned above, if you see similar symptoms, please open a new bug report. Thanks!Thanks @etraut-openai — appreciated the split clarification and the upcoming
ProcessHandle::terminate()fix. Standing by for the next release; will only open a fresh issue if a distinct residual shows up with a clean repro.