The 3am OOM culprit: the orphan processes your agent left behind


The laptop fan spins up again, localhost:3000 says the port is taken, and ps aux shows four node and python processes you do not recognize. Somewhere in the back of your mind: last week you asked the agent to “start a server and try it”, the session ended, the server did not. What these leftovers share is that nobody remembers them and nothing keeps accounts — which is the entire job of agent-janitor.

Where orphan processes come from

An agent’s tool call looks a lot like you typing in a terminal. You ask it to run npm run dev in the background, open a cloudflared tunnel, or attach a file watcher; the command returns, the process keeps running. When the session ends and its terminal goes away, Linux re-parents the survivor to PID 1 — that is what ppid == 1 means, and it is the most reliable “orphan” signature there is.

The cost is concrete: taken ports, eaten RAM, an OOM at 3am, and next time you start the agent it cannot reach its own service. Worse is the triage cost — you have no idea who started that python app.py.

It judges by evidence, not by vibes

agent-janitor grades processes instead of shooting at anything suspicious:

Evidence Confidence
Re-parented to init (ppid=1) and holds a port / dev-server traits / TTL exceeded confirmed
Re-parented to init (ppid=1) only likely
Dev traits and TTL exceeded, but the parent is still alive possible (never auto-swept)

Everything it reads is local: ppid and command lines from /proc, listening sockets from /proc/net/tcp{,6}, and an age threshold. Only confirmed and likely are killable.

Two classes are exempt by name: system daemons (multipathd, udisksd, ModemManager, systemd-* and friends), and host/VM management agents (tailscaled, cloud-provider agents, amazon-ssm-agent, walinuxagent, google_guest_agent). You add your own infrastructure to a whitelist:

janitor whitelist add ngrok
janitor whitelist add cloudflared
janitor whitelist add tmux

Whitelist entries are substrings matched against the command line (case-sensitive, regex-escaped, no injection). An empty array or a missing file means nothing is whitelisted.

Install, then scan before you sweep

It lives in the curated catalog (agent-janitor 0.3.1, community tier, requires Hermes >= 0.21.6). Install by name if your catalog lists it, otherwise from the repository as the README shows:

git clone https://github.com/ZidoCode/agent-janitor ~/.hermes/plugins/agent-janitor
hermes plugins enable agent-janitor

Now scan only — do not clean anything yet:

python3 ~/.hermes/plugins/agent-janitor/janitor_cli.py scan
python3 ~/.hermes/plugins/agent-janitor/janitor_cli.py sweep --ttl-h 6

That second command sounds alarming, but it is a dry run by default: decisions are printed and logged as would_kill, nothing dies.

Exactly one file unlocks real killing — <hermes-home>/plugin-data/agent-janitor/policy_mode, whose content must be precisely enforce:

echo enforce > ~/.hermes/plugin-data/agent-janitor/policy_mode

That design is worth copying: safe by default, and the risky state requires a written decision.

Before you enforce, do these five things

  1. Fill the whitelist first. Your tunnel, reverse proxy, tmux, anything “started by the agent but intentionally kept” goes in with janitor whitelist add.
  2. Read the dry-run output as the kill list. That is exactly what enforce will act on, no more and no less.
  3. Know who it hits. The rule is “re-parented to PID 1 for over a minute, not exempt, not whitelisted” — it does not check who started the process. A nohup-ed hermes gateway or a tmux server is fair game in enforce mode unless you whitelisted it, and when run as root that includes other users’ processes.
  4. Know what it leaves behind. spawns.jsonl records terminal commands that looked like they start background work, and sweeps.jsonl records every decision (would_kill or killed), so you can audit the sweep afterwards.
  5. Know the kill order. SIGTERM, wait two seconds, SIGKILL — and before the kill -9 it re-reads the command line, aborting if the PID was recycled. No fratricide by PID reuse.

Pair it with the ledger for attribution

On its own, janitor can only say “this process is an orphan”. With the same author’s agent-ledger installed, it reads the ledger and turns an anonymous python app.py into “spawned by this exact tool call on port 8080, 12 days ago”. The attribution is fuzzy — it matches on the executable’s basename — so treat it as a hint, not proof. The author labels it the same way.

Three limits you have to know

  • Linux only, because it reads /proc. macOS and Windows are not supported, so on a Mac this tool belongs on your servers, not your laptop.
  • Laptop suspend inflates uptime. Uptime is boot-relative and keeps counting while the machine sleeps, so TTL false positives are possible on machines that close their lid. On a server — the main target — this does not apply.
  • Whitelisting matches command-line substrings, so a whitelisted shell that then spawns something dangerous is invisible to it.

To shrink the “I don’t know what my agent did” problem generally: janitor covers the process dimension, skill fingerprinting covers the skill dimension, and cron doctor covers scheduled jobs. For the general install-and-pin routine, see the plugin catalog guide.