Isolated/temp-profile Chrome survives MCP death on macOS (puppeteer_dev_chrome_profile orphans, ppid=1)
Summary
On macOS, Chrome instances launched by chrome-devtools-mcp via Puppeteer's temporary profile (puppeteer_dev_chrome_profile-*) keep running after the MCP server process is gone. They reparent to launchd (ppid=1) and accumulate for hours.
This is not "please add a close-browser MCP tool" (#484) and not "idle page CPU in connected mode" (#2599). It is also distinct from #1765, where SIGTERM of a still-alive MCP server does clean up Chrome. The hole is: MCP dies without a graceful SIGTERM (crash, SIGKILL, host killing the agent), and the isolated Chrome tree is not in a process group / death-pipe that dies with it.
Environment
- macOS 26 (Darwin arm64, Apple Silicon)
[email protected](vianpx)- Chrome Stable 151.0.7922.174 (
/Applications/Google Chrome.app) for the orphaned trees - MCP clients on the same machine: Codex, Grok CLI, Cursor (each session starts a stdio MCP server)
- Some clients attach with
--browserUrl=http://127.0.0.1:9333to a shared Chrome for Testing. The leaked trees are not that shared instance.
Fingerprint of the leaked processes
Each orphan is a full headless Chrome root (plus ~6 helpers):
/Applications/Google Chrome.app/Contents/MacOS/Google Chrome
--headless=new
--enable-automation
--user-data-dir=/var/folders/.../T/puppeteer_dev_chrome_profile-<rand>
--remote-debugging-port=0
ppid=1 # launchd
That user-data-dir name is Puppeteer's mkdtemp('puppeteer_dev_chrome_profile-') when userDataDir is left unset — i.e. --isolated / temp-profile launch in src/browser.ts, not the persistent ~/.cache/chrome-devtools-mcp/chrome-profile and not --browserUrl attach.
Measured on one workstation (2026-08-26)
- 42 orphan Chrome roots matching the fingerprint above (plus ~300 helper processes).
- Ages from ~5 minutes to 9+ hours.
- No living Node/Puppeteer parent for any of them.
- The shared
--browserUrlChrome for Testing on:9333was healthy and not part of this set. chrome-devtools-mcpstdio servers for live agent sessions were still parented correctly; the leaked Chromes were from sessions that had already died.- MCP attach count that day (shared launcher log): 83
connecting mcplines. The local reaper only ran when the shared:9333Chrome itself restarted — so once that shared instance stays up, isolated leftovers are never reclaimed.
Killing only those 42 roots (puppeteer_dev_chrome_profile-*, --type= helpers excluded) left the shared :9333 instance and agent-browser Chrome for Testing untouched.
Expected
If the MCP server process exits for any reason — SIGTERM, SIGKILL, crash, stdin close — Chrome launched in isolated/temp-profile mode should die with it (main + GPU/renderer/utility/crashpad), and the temp profile dir should be removed.
Actual
Chrome is detached. After the MCP process disappears, macOS reparents the browser to launchd. --headless=new often does not exit on its own (Chromium 327583144). Trees sit until a human kills them.
Why this is on chrome-devtools-mcp (not only the client)
#1765 / gemini-cli#13391 correctly say: clients should SIGTERM the MCP server. That is necessary and not sufficient:
- Agent hosts routinely SIGKILL workers (timeouts, IDE/terminal kill, process replacement). SIGTERM handlers never run.
- Puppeteer launches Chrome as a detached process group. SIGKILL of Node does not cascade.
--isolatedis the recommended way to run multiple MCP clients in parallel (the error even says "Use--isolatedto run multiple browser instances"). Parallel Codex/Grok/Cursor sessions are the default in 2026, so isolated temp profiles are the common path — and each one can leak a full Chrome tree.
Related reports with the same fingerprint:
- kirodotdev/Kiro#8152 —
ps | grep puppeteer_dev_chrome_profileafter subagent end; MCP PPID=1 - openclaw/openclaw#85721 —
chrome-devtools-mcpprocesses accumulate,client.close()does not reap the npx tree - NousResearch/hermes-agent#29363 — subprocess leak across session/gateway restart
Suggested fix (server-side, SIGKILL-safe)
Do not rely on process.on('SIGTERM') + browser.close() for isolated Chrome. That path is already wired (closeBrowser() on SIGTERM/SIGINT/SIGHUP/stdin end) and still loses the tree when the process is SIGKILL'd.
What works in similar tools (vercel-labs/agent-browser#1148 death-pipe sentinel):
- Launch isolated Chrome in its own process group.
- Hold a pipe/socket in the MCP process; a tiny sentinel blocks on the read end and
kill(-pgid, SIGKILL)when the pipe EOF's (covers SIGKILL of Node). - Default idle timeout for isolated/temp-profile browsers (e.g. 1h).
agent-browser0.34 now does this; it was the only reliable reclaim when clients die uncleanly (vercel-labs/agent-browser#1536). - On next MCP startup, reap leftover
puppeteer_dev_chrome_profile-*Chrome roots whose parent is gone.
--browserUrl attach should not kill the remote browser. Isolated/temp-profile launch should.
Workaround we are using
External reaper (LaunchAgent every 5 min) that SIGTERM/SIGKILLs only:
ps -axo pid=,command= | awk '/puppeteer_dev_chrome_profile-/ && /Google Chrome/ && !/--type=/'
That does not touch --browserUrl shared Chrome or the user's interactive Chrome.app. It should not be required.
Happy to provide a full ps dump or a minimal repro script (spawn MCP with --isolated --headless, kill -9 the Node PID, watch Chrome stay on ppid=1).
Source: ChromeDevTools/chrome-devtools-mcp