In April 2026, a Claude Code user opened issue #43944: processes started by the agent's Bash tool, such as dev servers, builds and test runners, kept running after the session exited. They were "reparented to PID 1 (launchd on macOS) and continue running indefinitely," and the report says orphaned next dev servers "can grow to 8+ GB." The issue was closed for inactivity in June. An earlier report, #16198, showed a workerd process from a wrangler dev task nobody stopped, at 89.1% CPU and about 2.5 GB of RAM after roughly two days.

That is one agent. Run five in parallel worktrees and each can start a dev server, a test watcher, a type checker and a browser for its end-to-end tests. The turn ends. The processes stay. Your laptop fans are usually the first to tell you.

Killing everything called node is the wrong fix, because one of those is the server another agent is testing against right now. You want the one tree that belongs to a finished task. These eight commands find it on macOS and Linux, and each one comes with how to read what it prints. Most of the Linux tools here come from procps-ng, and pstree from psmisc. htop and lsof are their own projects. macOS ships BSD versions, and a few flags mean different things there.

Which process is eating the machine

1. ps with the columns that matter

Linux:

ps -eo pid,ppid,pgid,%cpu,rss,etime,args \ --sort=-%cpu | head -15 

macOS has no --sort. Use -r to sort by CPU or -m by memory:

ps -Ao pid,ppid,pgid,%cpu,rss,etime,command -r \ | head -15 

pgid is the process group ID. You will kill by that number in step 7, so note it now.

rss is resident memory in KiB on both systems (Linux ps(1), macOS ps(1)). A value of 2500000 is about 2.4 GB.

etime is wall-clock time since the process started, in the form [[DD-]hh:]mm:ss. A dev server showing 2-03:10:44 has been up for two days. If none of your agents has been running that long, it is a leftover.

On a node, next dev or test-runner process, a ppid of 1 usually means the parent exited and the process was adopted. On macOS the adopter is launchd, as in #43944. launchd is also the normal parent of every macOS app and daemon, so read a PPID of 1 alongside the command and etime. On Linux an orphan goes to the nearest living ancestor that registered as a subreaper (PR_SET_CHILD_SUBREAPER), or to init if there is none, so its PPID may not be 1. Adoption changes the PPID. The PGID stays what it was.

%cpu means different things on the two systems. Linux ps divides CPU time by the time the process has existed, over its whole lifetime. A server that idled for two days and started spinning a minute ago still shows a low number. macOS reports "a decaying average over up to a minute" of recent time. On Linux, ask top what is hot right now.

A tangled tree of process nodes growing out of five small terminal windows, with one branch highlighted
Five agents, five process trees, and only one of them is finished.

2. top and htop, sorted and filtered

top -o mem # macOS: physical memory footprint top -o %MEM # Linux: P and M re-sort live 

On macOS, mem is the process's "physical memory footprint" (top(1)). For a one-shot snapshot that includes the group ID, which you can paste into an agent's chat:

top -l 1 -o mem -n 10 -stats pid,pgrp,command,mem 

On Linux, -o takes a field name and P or M switch the live sort to CPU or memory (top(1)).

htop makes the filter interactive:

htop -t -F node 

-t starts in tree view. -F keeps only commands that match the term, case-insensitive and literal, with | between terms. Inside, F4 edits the filter, F5 toggles the tree, and F9 opens a signal menu for the selected process or every tagged one (htop(1)). htop -F fix-login only finds processes whose command line contains fix-login. Step 4 covers what that misses.

Which agent started it

3. The tree: pstree or ps --forest

Linux:

pstree -pga 48211 # tree rooted at one PID ps -ef --forest | less 

On Linux, pstree comes from psmisc. -p adds PIDs, -g adds process group IDs, -a shows arguments, and a PID argument roots the tree at that process. Point it at an agent's PID and you see what that agent started: the npm, the node under it, the bundler worker under that. ps -ef --forest draws the same parentage for the whole system as ASCII art.

macOS has neither out of the box. brew install pstree gets you Fred Hucht's pstree (2.40 on Homebrew), which has its own flags:

pstree -s fix-login # branches whose cmdline has it pstree -p 48211 # branches containing that PID 

Don't bring the Linux -g habit here. In this pstree, -g n picks the line-drawing character set, not process groups.

Orphans break the tree view. Once a dev server has been adopted, it no longer hangs under the agent that started it. That is why step 1 asked for the PGID.

4. pgrep -f scoped to one worktree

# Linux: -a prints the full command line pgrep -af "$HOME/code/wt/fix-login" # macOS: -lf prints full args; -a means ancestors pgrep -lf "$HOME/code/wt/fix-login" 

-f matches against the full argument list instead of the process name, and the pattern is an extended regular expression (pgrep(1)). On macOS, -a means "include process ancestors in the match list" (BSD pgrep(1)). That is a different job from the Linux -a.

The limit: pgrep -f only finds processes whose arguments contain the path. A npm run dev started inside the worktree has no path in its arguments. The worktree is its working directory, and step 6 is how you find those.

What it is holding

5. Every dev server holding a port

lsof -iTCP -sTCP:LISTEN -nP lsof -nP -iTCP:3000 -sTCP:LISTEN # one port 

lsof with -iTCP -sTCP:LISTEN lists only listening TCP sockets, and the man page notes -s needs -i with it. -n skips turning addresses into host names and -P keeps ports numeric, which can also make it faster. Read COMMAND, PID and NAME, where NAME holds the address and port. Two node processes on 3000 and 3001, with no agent working on the second, is your candidate.

Add -t to get bare PIDs for scripts: lsof -t -iTCP:3001 -sTCP:LISTEN. That gives you one PID. Feed it to ps -o pgid= -p <pid> to get the group, then go to step 7.

A power strip crowded with plugs, one plug glowing hot under a magnifying glass
The port list tells you which dev server is still plugged in.

6. Every process with a file open in the worktree

lsof +d ~/code/wt/fix-login # top level only: fast lsof +D ~/code/wt/fix-login # whole tree: slow 

A process whose working directory is the worktree shows up with FD set to cwd. txt is the program's own executable. This is how you catch the npm run dev that pgrep -f missed. +d searches only the top level of the directory, which is where most dev servers sit. +D goes all the way down, and the man page warns it "may process this option slowly and require a large amount of dynamic memory." With a full node_modules inside, expect to wait. Neither follows symlinks unless you add -x.

Kill the whole group, then check

7. kill -- -PGID

pgrep -l -g 48211 # macOS: who is in the group pgrep -a -g 48211 # Linux kill -- -48211 # SIGTERM to every member sleep 5 pgrep -g 48211 && kill -KILL -- -48211 

A negative PID names a process group. POSIX kill reads a leading negative number as a signal, so -- goes first. The spec's own example is kill -- -123 (POSIX kill). With no signal named it sends SIGTERM, which a process can catch to clean up. The kill(1) page says to prefer it over KILL for that reason. Send KILL only to what is still in the group a few seconds later.

Why the group and not the PID: a signal sent to one PID reaches that PID only. Whether its children exit too is up to the parent. The group catches the npm, the node and the workers in one go, unless one of them moved itself into a new group.

Read the pgrep -g list before you send anything. If the agent's own process is in it, the signal hits the agent too. pkill -g 48211 does the same job if you prefer it.

8. Did it help: vm_stat and free -h

macOS:

vm_stat 5 # first line totals, then deltas sysctl vm.swapusage # total, used, free swap 

Linux:

free -h free -h -s 5 # repeat every 5 seconds 

vm_stat prints Mach virtual memory statistics. With an interval, the first line is totals and each line after shows the change. Watch swapouts, the compressed data written to swap files. Non-zero changes line after line mean the machine is pushing memory to disk while you watch. sysctl vm.swapusage prints total, used and free swap. A non-zero swap-used total only says pressure happened at some point. The deltas tell you whether it is happening now.

On Linux, read the available column of free, an "estimation of how much memory is available for starting new applications, without swapping." Run it before and after step 7. If the number barely moves, the hog is still alive somewhere. Go back to step 2.

The whole list on one screen:

You want Linux macOS
Hottest processes ps -eo pid,pgid,%cpu,rss,etime,args --sort=-%cpu ps -Ao pid,pgid,%cpu,rss,etime,command -r
Live, by memory top -o %MEM or htop -t top -o mem
Who started what pstree -pga <pid> pstree -p <pid> (Homebrew)
One worktree, by args pgrep -af <path> pgrep -lf <path>
Ports held lsof -iTCP -sTCP:LISTEN -nP same
Open in the worktree lsof +d <path> same
Kill a group kill -- -<pgid> same
Memory pressure free -h vm_stat 5, sysctl vm.swapusage

Know which agent is done before you kill its tree

Everything above tells you what is running. None of it tells you whether an agent still needs it. A dev server under a finished task is garbage. The same server under a task that is mid-turn is what the agent is about to send a request to.

I build Hermes IDE, so weigh this part with that in mind. Hermes gives every agent session one status: working, needs approval, asked you, done, error or idle, and it says when a status is a guess (README). That is the input step 7 needs. A session marked done or idle is a candidate. One marked working is not. Needs approval or asked you means the agent is paused on you in the middle of a turn, so leave its tree alone.

Two agent sessions side by side in Hermes IDE, Codex and Claude Code, each in its own worktree, with the sessions sidebar on the left
Codex on orders-types and Claude Code on orders-api, each in its own hermes/ worktree, with the sessions sidebar showing status, context use, cost and branch.

Each task also runs on its own branch in its own worktree, so the path you give pgrep -f and lsof +d is that task's folder. New worktrees get their own ports too (README), so if your dev server listens on the port Hermes gave its worktree, a line in step 5's list points back at one task.

On macOS and Linux, quitting Hermes does not stop your agents. They keep working while Hermes quits, updates or crashes, and you reattach where they left off. If you want a tree gone, steps 1 to 7 are still how you do it. Hermes is free to download, source-available under BSL 1.1, and runs on macOS, Windows and Linux. The commands in this post cover macOS and Linux only.

If this was useful

Every command here answers a capacity question on one machine: who holds the CPU, the memory and the port, and what happens when the machine runs out. The System Design Pocket Guide: Fundamentals asks the same questions one level up, about the building blocks of systems that have to scale. If finding the hot process was the fun part, that book is the next step.

If you run several agents at once, download Hermes IDE and, if it saves you a hunt through ps, star it on GitHub.

System Design Pocket Guide: Fundamentals — Core Building Blocks for Scalable Systems