Find and kill the processes your coding-agent sessions left behind — dev servers, watchers, and background commands that outlived the session that started them.
Supported agents: Claude Code today. The tool is agent-agnostic above a small adapter layer, and ships only adapters that have been verified against a live install — the main verb here is
kill, and a wrong session attribution is worse than a missing one.agent-ps --discoverhelps you find an unsupported agent's markers so an adapter can be written from measurement.
$ agent-ps
ABANDONED (2)
PID PPID PGID AGE SESSION T PORTS COMMAND
79000 1 79000 4h 999123 2 3000 next-server (v14.2.3)
87387 1 87387 2d 999123 2d 8080 /bin/zsh -c node server.js
DETACHED (1)
PID PPID PGID AGE SESSION T PORTS COMMAND
44120 1 44120 12m 50332 2 5173 vite
$ agent-ps --reap
archived 4 log file(s) -> /Users/you/.agent-ps/logs
TERM -> group -79000 (79000)
TERM -> group -87387 (87387, 87388)
all groups exited on SIGTERM.
When an agent runs a background command, the intermediate shell exits and the
real process is reparented to launchd (ppid=1). The link back to the session
is destroyed, so you cannot find it by walking the process tree — and ppid == 1
on its own is useless as a filter, since a few hundred system daemons match it too.
| Agent | Status |
|---|---|
| Claude Code | full — live trees and reparented orphans |
| Codex | partial — live process trees only; it stamps no session id, so orphans cannot be attributed |
| OpenCode, Cursor, Aider, Goose, … | not implemented |
The mechanism below should apply to any agent that stamps its children with a marker
naming the owning session, and everything except the marker itself is already
agent-agnostic — but Claude Code is the only adapter anyone has measured. Adding one
is a small file: run agent-ps --discover to find your agent's markers, then follow
docs/adapters.md.
Adapters ship only once tested against a real install. A guessed session parser
misattributes processes, and the main verb here is kill.
A process's environment block outlives its parent. An agent that stamps its
children with a marker naming the owning session can therefore be traced after the
parent link is gone. Claude Code stamps CLAUDECODE=1 plus a socket path that
embeds the owning session's PID:
CLAUDE_CODE_MESSAGING_SOCKET=/tmp/cc-socks/50332.sock
That survives reparenting. An orphan can still name its own parent long after it stopped having one — which is enough to tell a dead session's leftovers apart from a live session's dev server, and from an app that merely inherited the variable years ago.
Details in docs/architecture.md.
Requires Node 18+ and macOS. (Linux is untested; Windows cannot work — see platform notes.)
git clone https://github.com/ssafayet/agent-ps.git
cd agent-ps
npm link # or: ln -s "$PWD/bin/agent-ps" /usr/local/bin/agent-psSymlink or npm link — do not copy bin/agent-ps on its own, it resolves
lib/ relative to itself.
Nothing else is required. The hooks and the menu bar are optional additions.
agent-ps # what is out there
agent-ps --reap --dry-run # what would die
agent-ps --reap # kill the abandoned ones
agent-ps --agents # which agents are supported, and their live sessions--reap targets only abandoned — processes whose owning session is gone. It
archives their logs before signalling, and signals process groups, so a whole
orphaned tree goes down together even when its individual members are
unattributable.
| State | Condition | Reaped by |
|---|---|---|
managed |
live descendant of a live session | never |
detached |
ppid=1, owning session still alive |
--reap --all |
abandoned |
ppid=1, owning session dead or socket stale |
--reap |
unowned |
marked by an agent, but its adapter cannot name the owning session | --reap --suspect |
suspect |
unattributable, but ppid=1 with an agent-project cwd |
--reap --suspect |
inherited-app |
marked, but lives under /Applications etc. |
never |
suspect is the one state that is a guess rather than a proof. It is listed so
you can see it, and never reaped without asking.
inherited-app exists because environment is inherited permanently. A GUI app
launched once from a session carries the agent's marker for the rest of its life —
during development OrbStack showed up as a two-day-old orphan attributed to a
long-dead session. Ancestry is not ownership.
| Architecture | attribution tiers, classification, blind spots, platform notes |
| Agent adapters | the adapter contract, finding markers, verifying a new agent |
| CLI reference | every flag of agent-ps and reap-run, exit codes, JSON schema |
| Logs | what can be recovered after the fact, and what must be captured before |
| Hooks | the PreToolUse indexer and the SessionEnd reaper |
| Menu bar | SwiftBar / xbar plugin |
| Troubleshooting | when it finds nothing, or too much |
| Known issues | open defects, with reproductions |
Menu bar. A SwiftBar plugin showing the same states in a tray menu, with per-group kill and a dry-run that shows the blast radius first. See docs/menu-bar.md.
Log capture. agent-ps --logs <pid> finds the output of a background command
even after its session exited. A PreToolUse hook makes the match exact, and
reap-run captures output for processes that would otherwise have none. See
docs/logs.md.
Automatic cleanup. A SessionEnd hook that reaps only the exiting session's
processes. See docs/hooks.md.
--reaptargets onlyabandonedby default. Live sessions' processes require an explicit--all; heuristic matches require an explicit--suspect.--dry-runprints the blast radius without signalling.- Logs are archived before anything is killed.
--kill-grouprefuses any PGID the tool did not itself attribute, so the menu bar cannot become a general-purposekill(-N).agent-psnever signals its own process group.
Signals are group-wide, so a protected process sharing a PGID with a target would die
with it — a group containing one is therefore skipped entirely, with the reason
printed. Unknown flags exit non-zero rather than falling through, so a mistyped
--dry-run cannot cause a real kill. --json omits process environments and masks
credential-shaped values in the command line by default, since an abandoned dev server
is exactly what holds your credentials; --unsafe-env returns both in full.
A marker only counts when it is genuinely a variable. ps eww prints the argv ahead
of the environment with no delimiter between them, so a command line that merely
mentions CLAUDECODE=1 used to read as a marked process — and be reaped as
abandoned. The argv is stripped before parsing, and the parse fails closed if it
cannot be.
The log store is 0700/0600: it holds captured output and the full text of every
background command.
Read known issues before relying on it in anger.
See CONTRIBUTING.md.