shoo
Set it up once. Your server stops filling up with the processes your coding agents leave behind.
Agents start MCP servers, headless browsers, dev servers and background tasks, then the session
ends and those processes keep running, holding hundreds of megabytes each. You shouldn't have to
notice, let alone remember to clean up. shoo runs quietly in the background, finds the
leftovers that are orphaned and idle, and removes them. You never have to run a thing.
pipx install git+https://github.com/avtomate/shoo
shoo schedule enable
That's the whole setup. Every hour a systemd timer runs shoo, and everything it removes is
recorded for you to review whenever you like (shoo log).
Recorded from real runs against simulated leftovers. They were started seconds earlier, hence
the 0m ages; the demo's config sets min_age to 0.
Status: alpha. shoo has been used on Ubuntu servers running agents under systemd, and is tested in CI on Ubuntu. Desktop Linux sessions, WSL and distros without systemd haven't been tried yet. If you'd like to see what it would do before it does anything, start with
shoo schedule enable --dry-run(below), and please report anything it gets wrong.
Quick start
Requires Linux and Python 3.11+. No dependencies, no root.
pipx install git+https://github.com/avtomate/shoo
shoo schedule enable # that's it: it now cleans up after your agents, hourly
Want to trust it first? Watch before it acts:
shoo schedule enable --dry-run # records what it would clean, kills nothing
shoo log # a few days later, see what it found
shoo schedule enable # looks right? go live
Prefer to run it by hand, or just curious what's lying around right now?
shoo # list leftovers (never kills anything)
shoo clean # clean them up (asks first)
Why it's safe to leave unattended
Nobody is watching a background job, so the timer is deliberately more cautious than running shoo by hand. Before it removes anything, a process must be:
- an orphan: the session that started it is gone, and it isn't attached to a terminal;
- a known agent or dev tool, not a database, a service or one of your own programs;
- old: running for at least 6 hours, and
- idle: shoo has watched the whole tree stay quiet for at least 2 hours, across runs.
Anything that something is connected to (a browser tab, an SSH tunnel) is left alone. It never
touches systemd services, containers, tmux, or another user's processes. Every run is logged,
and shoo schedule disable removes the timer. The full rules are in the next section.
How it decides
A process is a candidate only if all of these are true:
- It's a known agent or dev tool: MCP servers, headless browsers, Playwright drivers, JS and
Python dev servers, language servers, agent CLIs, background tasks from finished Claude Code
sessions,
tail -f-style watchers (shoo ruleslists them). Matching looks at the program and its first few arguments, never at directory names, so a project calledmy-mcpdoesn't make everything inside it look like an MCP server. - It's orphaned: the session that started it is gone (re-parented to init or your
systemd --user) and it isn't attached to a live terminal. Something launched through a shell wrapper (bash -c "npm run dev") counts as orphaned if the wrapper is. - It's old enough: running longer than
--min-age(1h by hand, 6h on the timer). - It's yours, and not a systemd service, a Docker or Kubernetes container, or
tmux,screenorsshd.
Its children go with it. A candidate tree is then held back (listed, not cleaned) when:
- someone is using it: something outside the tree is connected to a port it's listening on (a browser tab, an SSH tunnel), or it is connected to one of your own running programs (a debugger attached to your editor). Other outbound connections, like an agent's keep-alive to its API or a dev server's pool to a system database, don't count. Or:
- it hasn't been idle long enough: with
--require-idle(2h on the timer), shoo must have watched the whole tree stay under 1% CPU for that long, across runs. Children that already exited are counted too, so a loop that keeps spawning short commands isn't mistaken for idle. Or: - it might be a desktop app (unattended runs only): it lives in a desktop app's scope and
shoo can't confirm the app that owned it has exited. Interactive
shoo cleanshows it and lets you decide.
shoo scan --why lists your processes that were not picked, and why. It's the quickest way to
spot a leftover no rule catches yet.
Reading the output
| Column | Meaning |
|---|---|
MEM |
Proportional memory (PSS): shared pages are split between the processes sharing them, so the total is what you'd actually get back. Plain RSS would count Chrome's shared memory a dozen times. |
CPU% |
CPU over a short sample (--sample SECS, default 2). |
IDLE |
How long shoo has watched the whole tree sit idle; new the first time it sees it. |
CONN |
Live connections from outside the tree into a port it's listening on. |
PORTS |
Ports it's listening on. |
Commands
| Command | What it does |
|---|---|
shoo / shoo scan |
List leftovers. Changes nothing. --why adds what was skipped; --json for scripts. |
shoo clean |
List, ask, then SIGTERM, wait --grace seconds (default 5), SIGKILL what's left. -y skips the prompt; --dry-run records instead of killing. |
shoo schedule enable |
Install an hourly systemd user timer. --dry-run, --every, --min-age, --require-idle. |
shoo schedule status / disable |
Show the mode and next run / remove the timer. |
shoo log |
What was cleaned, or would have been in dry-run. -n for more. |
shoo rules |
What it looks for. |
Options for scan and clean:
| Option | Default | |
|---|---|---|
--min-age DURATION |
1h |
Ignore anything younger (30m, 2h, 1d). |
--require-idle DURATION |
off | Only clean trees watched idle this long. |
--include-active |
off | Also clean trees that something is connected to. |
--protect REGEX |
Never touch matching command lines. Repeatable. | |
--sample SECS |
2 |
CPU measurement window; 0 to skip. |
--config PATH |
~/.config/shoo/config.toml |
A file you name must exist. |
Exit codes: 0 success; 1 you declined at the prompt, or a process couldn't be signalled;
2 bad arguments, clean run without a terminal and without --yes, or run as root;
130 interrupted with Ctrl-C (anything already sent SIGTERM is logged, and isn't force-killed).
Configuration
# ~/.config/shoo/config.toml
min_age = "2h"
protect = ["my-staging-app", "--port 8080"] # regexes against the command line
Safety and privacy
- Read-only by default.
scannever signals a process; the only thing it writes is the idle history that lets shoo tell how long something has been quiet.cleanasks first and refuses to run without a terminal unless you pass--yes.--dry-runnever signals anything. - No PID-reuse accidents. It re-checks each process's start time right before signalling, so a recycled PID is never hit.
- Graceful first: SIGTERM, a grace period, then SIGKILL only for what's left.
- Never reads environment variables (
/proc/<pid>/environ), where secrets live. It reads only command lines, and redacts obvious credentials (--token ...,KEY=...,user:pass@,sk-...) before showing or logging them. - Local only. No network access, no telemetry. It only ever acts on your own user's processes,
and keeps its state (idle history, log) in
~/.local/state/shoo, readable only by you.
FAQ
I have "zombie" processes eating RAM. Will this fix that?
Probably, but they're likely not zombies. A true zombie (Z in ps) has already exited and uses
no memory; it disappears when its parent does, and nothing can kill it. What uses memory is an
orphaned live process, which is what shoo cleans. If real zombies exist, shoo tells you and
explains why there's nothing to do.
Will it kill my Docker containers, tmux sessions, or the agent I'm running right now?
No. Containers and systemd services are excluded by cgroup; tmux, screen and sshd are never
touched. Anything in a live terminal, or whose parent is still running (like the tools your current
agent session is using), isn't orphaned and so isn't a candidate. Neither is an app you started
from GNOME's launcher, even though it may look parentless (once it exits, whatever it leaves
behind is fair game).
What actually gets left behind? In testing with Claude Code, a session that exits normally cleans up after itself. One that dies abruptly (killed, crashed, out of memory, a dropped connection) leaves its background shell tasks running indefinitely; MCP servers like the official filesystem server exited on their own. Dev servers and browsers started by an agent's tasks go with the task's tree, so shoo cleans them too.
Why not earlyoom or systemd-oomd? They solve a different problem. They step in when memory is nearly gone and kill whatever is biggest, which might be the thing you care about. shoo is targeted and early: it removes known leftovers before they add up. They work well together.
It cleaned something I wanted.
Add it to protect in the config. Please also open an issue with the command line (redact anything
private), so the rule can be tightened.
Does it need root? Does it work on macOS?
No root: it acts on your own processes. In fact shoo clean and shoo schedule enable refuse to
run as root, because root's "own processes" are system daemons (--allow-root overrides, but run
it as the user whose agents leave processes behind instead). Linux only for now; it reads /proc.
Uninstall
shoo schedule disable # remove the timer, if you enabled it
pipx uninstall shoo
rm -rf ~/.local/state/shoo ~/.config/shoo
Contributing
Reports of what shoo gets wrong on real machines are the most useful contribution: something it flagged that it shouldn't have, or a leftover it missed. There are issue templates for both. To add a rule or send a fix, see CONTRIBUTING.md; changes are tracked in CHANGELOG.md. Please report security problems privately, as described in SECURITY.md. This project follows a code of conduct.
License
Metadata
Release files for shoo 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| shoo-0.1.0.tar.gz | 43.9 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| shoo-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 75.5 kB
Release files / shoo-0.1.0.tar.gz
| Download URL | shoo-0.1.0.tar.gz |
|---|---|
| Size | 43.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
eddfc84adcad19c68cfb25f1f653ca7622ea0e6eb9488428cba09302aceda700
|
|
BLAKE2b-256 checksum How to use checksums |
d78cd95783d6bc0db15fcec3db45fcaaf80d78f3b32d083ea8ea27637719e481
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logRelease files / shoo-0.1.0-py3-none-any.whl
| Download URL | shoo-0.1.0-py3-none-any.whl |
|---|---|
| Size | 31.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3927057d152124bc2ccd4209551a1cb4be8246c217c82029da500ec8891be4b5
|
|
BLAKE2b-256 checksum How to use checksums |
6dff9dde183e1ccdf15c634ed81a97066a0c66dfb1ea34c96be957bfc6fd0ee7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logSecurity for shoo 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for shoo-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.