Nobody Owns What the Agent Leaves Running
My machine ran out of resources, so I handed an agent four screenshots and typed one word: fix. It did a good job. A day later, twenty-two connector processes were still running, every one of them marked disabled in config. Nothing had failed — and that turns out to be the interesting part.
Nino Chavez
Product Architect at commerce.com
My machine ran out of resources and I had to reboot it. So I took four screenshots of Activity Monitor, handed them to an agent, and typed one word: fix.
It did a good job. It walked the process tree, found connector processes that had outlived the clients that started them, rewrote the configs, and reported back with before-and-after numbers.
A day later, twenty-two connector processes were still running. Twelve Chrome DevTools. Five Context7. Five Playwright. Every one of them reads enabled = false in config.
I looked at Activity Monitor and did not believe what I was seeing.
The answer was right, though. So was the config. A flag decides what starts next time. It has no reach into what is already running. Nothing in the loop does. Not the client, not the session, not the cleanup agent that reported zero while those twenty-two processes sat there at a full day of uptime. Handing work to an agent leaves things behind, and no step in the loop is responsible for ending them.
The processes were already running before the fix ran
The whole batch started inside seventeen seconds, between 07:02:02 and 07:02:19 the previous morning. The fix that was supposed to clear them finished about twenty minutes later.
So this was not a cleanup that missed a few. It was a cleanup that ran, changed the config, reported success, and never touched the running population — because nothing it changed applies to a process that already exists. A day and twenty-two minutes later they were all still there.
The client would not touch them, and it was right not to
Two of those processes belonged to the Codex app server. The other twenty belonged to four Claude sessions in terminal tabs.
Neither client can clean up after the other, and neither should try. A tool that kills processes it did not start is a worse tool than one that leaves a few strays behind. Codex leaving another client’s sessions alone is correct. It is also why the count never went down. Everybody scoping their cleanup correctly is exactly what produces a pile in the middle that belongs to nobody.
A session is just a tab somebody left open
None of those twenty were orphans. Each had a live parent, still attached to a terminal, a day old.
That is the real lifecycle rule, and no config file can express it. A connector lives as long as the session that started it. A session lives as long as the tab. Four tabs left open overnight is not a leak by any definition the system uses — it is just a tab. It is also the entire reason twenty-two processes survived a fix aimed squarely at them.
The audit was true for about five minutes
The agent’s three main conclusions all fell over within a day, and they fell over for the same reason. A reading taken at one moment cannot tell you what is running at the next one.
It reported:
every live connector is a descendant of Codex, not Claude
A day later, twenty of the twenty-two belonged to Claude. That measurement got taken minutes after a reboot, at a moment when Claude happened not to be running. A snapshot of one moment became a claim about the system.
It reported:
removed 17 leaked MCP process groups. They remain at zero.
The 07:02 batch survived and kept running. The removal may well have happened. “They remain at zero” is the part that could not be true, because it is a claim about the future built from a single reading.
It reported disabling the Chrome DevTools connector. That connector was never in the plugin system at all. It is a per-project mcpServers entry under /Users/nino in ~/.claude.json, so disabling the plugin could not reach it. Twelve processes were running from a config surface the fix never touched.
There are four places a connector can start, and I only pinned one
This is the structural thing underneath all three failures: there is no single off switch. A connector can be started by a global plugin setting, by that setting’s copy in dotfiles, by a per-project mcpServers block, or by project settings. Turn it off in one and it keeps running from another. Nothing reconciles the four.
Chrome DevTools I pinned exactly — one entry, one file, one path. Context7 I did not. Five processes were running and I never determined which of the four surfaces was starting them.
I am leaving that gap in rather than tidying it away, because it is not a footnote on the finding. It is the finding, demonstrated twice in one measurement — once by a surface that resolved and once by a surface that did not.
The only fix that stuck removed the thing entirely
mcp-remote went to zero and stayed there.
That fix was a different kind. It did not flip a flag on something that starts processes. It replaced the something: a local Node bridge swapped for a native HTTP connection. Now there is no process to start, no surface that can start one, and nothing for a stale session to hold onto. Every fix that changed a value left a running population behind. The one that changed a mechanism did not.
Worktrees and browser profiles did the same thing
This machine has produced this shape twice before, and both times the leftovers were something an agent created on purpose.
One repo accumulated fifty-two git worktrees holding 74 GB, sixty-eight of those gigabytes in Rust build directories. They were invisible for a specific reason: .worktrees/ is ignored machine-wide, so they never showed up in git status. The rule that requires a worktree per parallel session has no matching rule that ends one. None of the fifty-two had been merged.
Browser profiles went the same way. A hundred and two of them, 74 GB. Switching to a shared profile by default brought it to twenty-four profiles and 17 GB — a change to what gets created, not a pass that deleted what had piled up.
All three I found the same way, which is to say I did not find them. Something ran out. A full disk. A reboot. Only the worktrees ended up with anything that ends them, a reaper I wrote after already hitting the wall — a fix built at the point of pain instead of the point of creation. The profiles got a change to what gets created. The connectors got neither.
The check is one command
ps -eo pid,etime,rss,command | grep -iE '(mcp|context7|playwright|chrome-devtools)' | grep -v grep
That is the whole diagnostic. What is running, next to what the config claims. If the two disagree, the config is describing the next session rather than this one, and the difference is measured in uptime.
I ended up writing a longer version, connector-reaper.py, for two reasons. It reports the ratio the one-liner cannot — connector processes against loaded sessions, which is where duplicate-spawn shows up. And it redacts credentials before printing, because one of those command lines carries an API key in its arguments, readable by anything on the machine that can run ps. It reports by default and reaps only processes whose owning client is gone, which is nearly none of them: on this machine, the count that is actually safe to kill is zero.
What the config says and what is running are two questions
Every mechanism here is doing its job. The flag disables. The client scopes cleanup to its own children. The session holds its connectors exactly as long as the session lasts. The isolation rule makes a worktree per session because that is the point of it.
The gap is not inside any of them. It sits between them, in the space where something outlives the task that made it and nobody has a claim on it. An agent finishing its work and the leftovers going away are two different events, and only the first one is anything the loop can see.
Four terminal tabs sat open with nobody typing in them, each holding its connectors alive a full day after the last prompt. The config said off. The tabs said otherwise, and the tabs are what runs.