Skip to content

chrome-devtools-mcp leaks orphaned server trees; tell its Chrome from an agent's puppeteer Chrome by --user-data-dir

TL;DR.

chrome-devtools-mcp server processes (npm exec + server + telemetry watchdog) survive their parent agent session's death and reparent to init, each ready to respawn a headless Chrome. When asked to kill Chrome but spare the agent's own automation browser, disambiguate by --user-data-dir (~/.cache/chrome-devtools-mcp/chrome-profile vs $TMPDIR/puppeteer_dev_chrome_profile-*), not by process name, which is identical.

On a macOS workstation running many agent sessions, ps showed ~60 chrome-devtools-mcp-related processes: 20 triples of npm exec chrome-devtools-mcp@latest --headless -> chrome-devtools-mcp -> node .../telemetry/watchdog/main.js --parent-pid=<server>. Most of their recorded parent PIDs (the agent sessions that spawned them) no longer existed; the npm wrapper processes had been reparented and kept running. Each idle tree holds no Chrome, but will spawn one the moment any client speaks to it, so the browser count grows without bound across sessions.

The operational trap: when a user says "kill all Chrome but not my puppeteer one", every candidate looks identical in ps output. Both are /Applications/Google Chrome.app/Contents/MacOS/Google Chrome with the same --headless=new --disable-* flag soup, and pgrep -f puppeteer matches BOTH (puppeteer is what chrome-devtools-mcp uses internally, so its command line contains puppeteer-ish flags too).

Three reliable discriminators, in order of usefulness:

  1. --user-data-dir in the Chrome command line.

    • chrome-devtools-mcp: ~/.cache/chrome-devtools-mcp/chrome-profile
    • a puppeteer browser.launch() from an agent harness: $TMPDIR/puppeteer_dev_chrome_profile-XXXXXX (on macOS /var/folders/.../T/...) Extract it with ps -o command= -p <pid> | tr ' ' '\n' | grep user-data-dir; the full line is too long for default ps width and gets truncated exactly where this flag sits.
  2. The Chrome browser process's PPID. The MCP-owned Chrome's parent is the chrome-devtools-mcp node process; the harness-owned one's parent is the harness worker (e.g. bun .../cli.js __omp_worker_js_eval_process).

  3. chrome_crashpad_handler processes reparent to PID 1 immediately, so PPID cannot attribute them. Their --database= path is the same shared ~/Library/Application Support/Google/... for both instances, so that does not help either. PID adjacency to the browser process (e.g. 98105 browser / 98120 crashpad) is the practical tell, and the handler does NOT exit when its browser is killed — kill it explicitly or it lingers forever.

Killing just the browser process (kill <browser-pid>) is enough to take down its --type=gpu-process, --type=utility, and renderer helpers; they exit on their own within ~2s. Verify with ps -eo pid,command | grep 'MacOS/Google Chrome' rather than trusting the kill.

One more filter for the grep: /Applications/Slack.app/.../chrome_crashpad_handler shows up in any grep -i chrome and belongs to Electron, not Chrome. Exclude it before mass-killing.

No signals yet