Orphaned Claude Code Processes: Kill Only the Dead Ones
The usual advice works the way a grenade works. It also kills the session you are in, its subagents mid-write, and processes that merely mention a .claude path.
You closed the terminal window. Days later Activity Monitor shows a column of claude processes with multi-gigabyte resident memory, attached to nothing. The advice you will find for this is almost always pkill -9 -f claude, and it works, in the sense that a grenade works. It also kills the session you are currently working in, the subagents it spawned mid-write, and — because of how the pattern matches — a set of processes that have nothing to do with Claude at all. Here is how to identify only the dead ones, and kill only those.
Why it happens
On Unix, when a parent process dies, its children are not killed. They are reparented to init (pid 1) and keep running. A terminal emulator normally handles this by sending SIGHUPto its foreground process group when the window closes — that is what the “HUP” in hangup means, and it is a signal from the era of actual telephone lines.
A long-running interactive CLI can miss that. If it does not handle SIGHUP, or the emulator does not send one on that particular close path (closing a window vs quitting the app vs killing the pane are three different code paths, and IDE-integrated terminals are a fourth), the process survives its parent, gets adopted by pid 1, loses its controlling terminal, and keeps its entire heap resident. Nothing is attached to it. Nothing will ever be attached to it again. It is pure resident memory until you or a reboot ends it.
The processes are not stuck, and they are not doing anything. They are waiting for input from a terminal that no longer exists.
This is a well-documented rough edge — there is a cluster of open reports on the Claude Code issue tracker covering the terminal-close path, the desktop-app quit path and the subagent case. Treat the cleanup below as ongoing hygiene rather than a one-time fix, and re-check after upgrades.
Find them: three columns tell you everything
ps -Ao pid,ppid,tty,etime,command | grep -i '[c]laude'
The [c]laude bracket trick stops grep from matching its own command line. Then read three columns:
ppid— the parent.1means the parent is gone and the process was reparented to init. This is the strongest single orphan signal.tty— the controlling terminal.??on macOS (or?on Linux) means no terminal. A live interactive session shows a real tty likes004.etime— elapsed time, asDD-HH:MM:SS. A process with days on it that you do not recognise is not a session you forgot about ten minutes ago.
Add the working directory and you can usually name the session on sight. On macOS:
lsof -a -p <pid> -d cwd -Fn # prints n/path/to/working/dir
Not everything detached is dead
This is the step the one-liners skip, and it is the one that matters. ppid 1 plus no tty is evidence, not a verdict. Things that legitimately look exactly like that:
- Anything you started with
nohup,disown,tmux,screen, or a background&— deliberately detached, deliberately parentless, possibly halfway through a long job. - A session running under a GUI app rather than a terminal. Its parent is the app, not a shell, and it has no tty.
- MCP servers. A local MCP server is a child process of the client that launched it. When that client is alive, the server is doing its job with no terminal of its own. When the client is gone, the same server becomes a genuine orphan. Same shape, opposite verdict — the difference is entirely whether the parent still exists.
So the actual test is not “is it detached” but “is anything still talking to it”. Parent gone plus no tty plus long idle plus a working directory you are not currently in is a confident yes. Any one of those alone is not.
Why pkill -9 -f claude is worse than it looks
Two independent problems, and the second one surprises people.
It kills your live sessions too
pkill -f matches every process whose full command line contains the pattern, with no regard for parent, terminal or age. The session you are in right now matches. So does every subagent it spawned. With -9 they get SIGKILL, which cannot be caught — no flush of the session transcript, no cleanup of temporary state, no orderly shutdown of the MCP servers it owns. If your goal was to reclaim memory and your result was a lost hour of context, that is a bad trade you made with one command.
It matches processes that are not Claude at all
-f matches the whole command line, and Claude Code's own machinery puts its config path into the command line of ordinary shell processes. Here is a real ps line from the machine this page was written on — an unremarkable zsh running an unrelated command:
50802 358 /bin/zsh -c source /Users/…/.claude/shell-snapshots/snapshot-zsh-….sh …
That is not a Claude process. It matches -f claude because of a path. Any script, editor, or log tail whose arguments mention a .claude directory matches too. Broad pkill -f patterns are a classic way to take out something you never intended to touch.
The safe sequence
- List, and look.
ps -Ao pid,ppid,tty,etime,command | grep -i '[c]laude'. Do not pipe anything into a kill yet. Read it. - Account for every live session. Note the pids with a real tty and the ones whose parent is a terminal or IDE you have open. Those are off limits. What remains are candidates.
- Confirm each candidate is cold. Parent is
1, no tty, longetime, and a working directory you are not using. Check the directory with thelsofline above if you are unsure — one lookup is cheaper than one wrong kill. - Send
SIGTERMfirst, by pid.kill 12345 12346. Explicit pids, no pattern.SIGTERMis catchable, so a process that has anything to flush gets to flush it. - Wait, then re-list. Most will be gone. Give it a couple of seconds.
- Only now,
SIGKILLthe survivors.kill -9 12345, again by pid. A process that ignoredSIGTERMhas earned it. - Sweep the MCP servers the same way. Killing a client does not always reap its server children. Re-run the listing with
grep -i '[m]cp'and repeat steps 3 to 6 for anything now sitting onppid 1.
Why the list is longer than the number of sessions you ran
A local MCP server uses the stdio transport: the client launches it as a child process and talks over its stdin and stdout. There is no shared broker and no daemon, so every client connection gets its own copy. Four sessions with the same server configured at user scope is four server processes.
And one registration is usually more than one process, because a launcher does not replace itself with the server. Real ps output from this machine, for a single server name registered twice:
75398 74931 …/Helpers/disclaimer /opt/homebrew/bin/uv run blender-mcp 75399 75398 /opt/homebrew/bin/uv run blender-mcp 75424 75399 …/blender-mcp/.venv/bin/python … 75400 74931 …/Helpers/disclaimer /opt/homebrew/bin/uv run blender-mcp 75403 75400 /opt/homebrew/bin/uv run blender-mcp 75474 75403 …/blender-mcp/.venv/bin/python …
Two independent chains of three: a wrapper, a launcher, and the server. Multiply by servers, by sessions, by the sessions that leaked instead of exiting, and the arithmetic explains the gigabytes without anything being broken. Kill the head of a chain and the rest usually follows; check, because “usually” is not “always.”
Reducing how often this happens
- Exit sessions, do not close windows.
/exitorCtrl-Dgives the process the clean shutdown path. Closing the window relies on the signal path that is exactly the thing failing here. - Sweep on a schedule you actually keep. Start of day is enough. This accumulates slowly and is invisible until it is expensive.
- Have the agent check at session start. Cheaper than remembering, and it means the agent knows the machine state before it reasons about it.
Making the check something the agent runs
The listing above is a thing you have to remember to do. The more useful version is the agent doing it, because the failure this prevents is not just wasted RAM — it is an agent that tells you a service is running because a note from three weeks ago said so. Model memory is a cache with no invalidation.
whats-running-mcp is the free MIT MCP server we wrote for exactly this. Its agent_sessions tool returns live agent processes split into terminal-attached and detached/orphan, each with pid, uptime and working directory — the classification in step 3, done for you. listening_portscovers the other half: every TCP LISTEN socket with the process that owns it, so a port held by a dead session's dev server stops being a mystery.
claude mcp add --scope user whats-running -- npx -y whats-running-mcp
It is detect-only, by construction. Every command it runs is a fixed binary with fixed flags via execFile — no shell, and nothing derived from model input is executed. It will never kill anything, which is deliberate: deciding what is dead is the part that needs a human, and this article is why. Source on GitHub, package on npm, and the rest of the free set on our agent tools page.
Summary
- Orphans exist because children survive dead parents; a missed
SIGHUPis the cause. ppid 1+ no tty + longetime+ a directory you are not in = confident orphan. Fewer than all four = check.pkill -9 -f claudekills your live sessions, and matches unrelated processes whose command line merely mentions a.claudepath.SIGTERMby explicit pid first,SIGKILLonly for survivors.- Sweep the MCP server children afterwards — they are not always reaped with the client.
Related reading: how to see which MCP servers are actually running separates configured from connected from running, and running multiple coding agents on one machine covers the three collisions that come with the territory once orphans are handled.