Skip to content

Isolated/temp-profile Chrome survives MCP death on macOS (puppeteer_dev_chrome_profile orphans, ppid=1) #2621

Description

@pedroknigge

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)
  • chrome-devtools-mcp@1.8.0 (via npx)
  • 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:9333 to 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 --browserUrl Chrome for Testing on :9333 was healthy and not part of this set.
  • chrome-devtools-mcp stdio 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 mcp lines. The local reaper only ran when the shared :9333 Chrome 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:

  1. Agent hosts routinely SIGKILL workers (timeouts, IDE/terminal kill, process replacement). SIGTERM handlers never run.
  2. Puppeteer launches Chrome as a detached process group. SIGKILL of Node does not cascade.
  3. --isolated is the recommended way to run multiple MCP clients in parallel (the error even says "Use --isolated to 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:

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):

  1. Launch isolated Chrome in its own process group.
  2. 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).
  3. Default idle timeout for isolated/temp-profile browsers (e.g. 1h). agent-browser 0.34 now does this; it was the only reliable reclaim when clients die uncleanly (vercel-labs/agent-browser#1536).
  4. 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).

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions