mem-report
Find and clear CLI agent processes that outlived their session and are quietly burning RAM.
Coding agents (Claude Code, Codex, Gemini CLI, aider, …) shell out constantly — to ugrep, rg, find, git, node. When a session dies, those children are reparented to init and keep running. A search with a catastrophically backtracking regex can sit at 9 GiB for sixteen hours with nothing pointing at it, because the process that started it is long gone.
mem-report looks at the whole process tree instead of the session process, so a runaway grep is attributed to the agent that spawned it — and can be cleared safely.
Why per-process memory reporting misses this
A watcher that samples "every Claude process" reports a healthy 300 MB session while its orphaned grandchild holds 9 GiB. The memory is real, the session looks fine, and nothing correlates the two.
PID RSS_MB AGE <- what per-process sampling shows
3894245 241 00:04:12 one healthy session
PID RSS_MB AGE <- what is actually resident
2496994 9252 15:49:55 orphaned ugrep, parent died hours ago
3869453 8435 00:23:47 orphaned ugrep under an orphaned agent shell
Installation
uv tool install mem-report
Linux only — it reads /proc directly and has no runtime dependencies. Both memr and mem-report are installed as commands. Run memr -v to check the installed version.
Usage
memr # the report (default command)
memr --all # plus the top memory consumers system-wide
memr --explain # why each stray was or was not selected
memr --json # machine-readable
memr reap # show the plan, then ask before signalling
memr reap --dry-run # never signals, whatever happens
memr reap --yes # no prompt
memr reap --pid 12345 # clear a specific tree
memr watch # sample process trees into a history log
memr watch --once # single sample, for cron
Output
mem-report ASUS-A16 2026-08-05 03:13:36
RAM 19.5 GiB total 19.1 GiB used 0.5 GiB available 97% used
Swap 8.0 GiB total 6.8 GiB used
OOM kills since boot: 0
SESSIONS (1)
PID AGENT RSS SWAP SUBTREE AGE FLAGS COMMAND
3894245 claude 241 76 317 00:04:12 - claude
STRAY PROCESSES (3)
PID TOOL RSS SWAP AGE FLAGS REAP COMMAND
2496994 ugrep 9252 2046 15:49:55 runaway orphan blocked yes ugrep -G -oin .{0,60}leech…
3869452 claude-shell 1 0 00:23:47 runaway orphan yes /bin/bash -c source /home…
3869453 ugrep 8435 1890 00:23:47 runaway blocked - ugrep -noE [^.]{0,45}(rat…
HISTORY (5,658 samples since 2026-08-04T15:57:52-03:00)
peak footprint 11,298 MB pid 2496994 ugrep at 02:59:32
over 2,048 MB 2 process(es) <-- investigate
2 process(es) reapable, holding 19.0 GiB
KILL THEM
memr reap --yes reap all 2 flagged above
memr reap --pid 2496994 --yes reap only ugrep (11,298 MB)
kill -9 2496994 3869452 skip memr, signal directly
pid 2496994 in uninterruptible sleep (D): SIGTERM cannot land, memr escalates to SIGKILL on its own
SUBTREE is the headline number: the session's own RSS plus every descendant's RSS and swap. A session at 241 MB with a 9 GiB subtree is the case this tool exists to surface.
Every report ends with the command that clears what it just found, pid included — copy the line, paste it, move on. Nothing suggested there can silently fail: processes owned by another uid are flagged foreign and excluded from the commands, called out separately instead.
When nothing is auto-reapable but something heavy was deliberately spared, the footer offers the override instead:
nothing auto-reapable, but 1 heavy process(es) were spared
KILL THEM ANYWAY (name the pid to override the guard)
memr reap --pid 3869453 --yes ugrep, 10,325 MB — runaway under live session 3894245
What counts as a stray
A process is reapable only when every one of these holds:
- it is orphaned (
PPID 1) — its spawning session is gone - it has no controlling terminal
- it is older than
--min-age(default 60s, so processes still starting up are never touched) - it is owned by you (or you are root)
- it is neither the running
memrprocess nor any ancestor of it - it is one of:
- an agent session (
claude,codex,gemini,aider,copilot,cursor-agent,opencode,goose,amp,qwen,crush) - a transient tool — search (
ugrep,rg,grep,find,fd, …), text (sed,awk,jq, …), or VCS (git,hg, …) - an agent-spawned shell, identified by its shell-snapshot path
- an orphaned shell currently running a transient tool — the wrapper case
- an agent session (
Everything else is reported but never selected.
What is deliberately spared
| Case | Why |
|---|---|
Orphaned node, python, tsc, cargo, … |
These legitimately run detached. Opt in with --include-persistent. |
| An orphaned shell with no transient child | A detached bash daemon (a watcher, a supervisor) looks exactly like an orphaned agent shell. Requiring a live search tool underneath separates them. |
| A runaway under a live session | The session may still be using it. Flagged runaway; the footer prints the --pid command to override. |
| Anything owned by another user | Flagged foreign, reported, never signalled — and never suggested as a command, since it could not land. |
| Processes on a terminal | Someone is attached. |
Children of a reapable root are killed with it — plan expands each root to its full subtree and signals deepest-first, so a wrapper shell cannot outlive its worker or vice versa.
Signal escalation
SIGTERM, then SIGKILL after --grace seconds (default 5).
This matters more than it sounds. A process thrashing inside the memory cgroup's reclaim path sits in uninterruptible sleep (D state) and will not act on SIGTERM — the signal is queued but never handled. Targets in D state are marked in the plan, and escalation is what actually clears them.
memr reap --signal KILL # skip straight to SIGKILL
memr reap --no-escalate # SIGTERM only, never escalate
memr reap --grace 30 # give slow shutdowns longer
Watch mode
memr watch samples every 30s into a JSONL log ($XDG_STATE_HOME/mem-report/history.jsonl, default ~/.local/state/mem-report/history.jsonl), rotating at 10 MB. memr reads it back for the HISTORY section, so peaks that happened while you were away are still visible.
As a systemd user service:
[Unit]
Description=Sample CLI agent process trees for memory growth
[Service]
Type=simple
ExecStart=%h/.local/bin/memr watch
Restart=always
RestartSec=10
Nice=10
MemoryMax=64M
[Install]
WantedBy=default.target
systemctl --user enable --now mem-report-watch.service
Exit codes
| Code | Meaning |
|---|---|
| 0 | success |
| 1 | report --exit-code found reapable processes; or reap left something alive |
| 2 | not a Linux host with /proc |
| 130 | interrupted |
report exits 0 by default even when it finds strays. Pass --exit-code to make it fail for monitoring.
memr --exit-code --no-history || notify-send "stray agent processes"
Options
| Flag | Applies to | Default | Meaning |
|---|---|---|---|
--threshold-mb MB |
all | 2048 | subtree footprint that counts as runaway |
--min-age SECONDS |
all | 60 | ignore orphans younger than this |
--stale-age SECONDS |
all | 14400 | age at which an orphan is flagged stale |
--include-persistent |
all | off | also reap orphaned runtimes and build tools |
--json |
report | off | machine-readable output |
--all |
report | off | add system-wide top memory consumers |
--explain |
report | off | print the decision for every stray |
--exit-code |
report | off | exit 1 when strays are reapable |
--no-history |
report | off | skip the watch-log summary |
-y, --yes |
reap | off | skip the confirmation prompt |
-n, --dry-run |
reap | off | print the plan, never signal |
--pid PID [PID …] |
reap | — | reap these trees regardless of heuristics |
--signal NAME |
reap | TERM | first signal (TERM, KILL, INT, HUP) |
--grace SECONDS |
reap | 5 | wait before escalating to SIGKILL |
--no-escalate |
reap | off | never follow up with SIGKILL |
--interval SECONDS |
watch | 30 | seconds between samples |
--once |
watch | off | single sample, then exit |
--log PATH |
report, watch | see above | history log location |
Colour is disabled automatically when stdout is not a terminal, and honours NO_COLOR.
Development
uv venv && uv pip install -e '.[dev]'
uv run ruff check . && uv run ruff format --check .
uv run mypy src tests
uv run pytest
Prior art
legacy/ holds the two bash scripts this grew out of — claude-mem-report and claude-memwatch. They sample per-process RSS for Claude Code only. claude-mem-report carries the process-tree attribution fix that motivated the rewrite; both are kept for reference and are superseded by memr.
License
MIT
Metadata
Release files for mem-report 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 | |
|---|---|---|---|
| mem_report-0.1.0.tar.gz | 50.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mem_report-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 77.8 kB
Release files / mem_report-0.1.0.tar.gz
| Download URL | mem_report-0.1.0.tar.gz |
|---|---|
| Size | 50.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
274213395dbc99ac7c47cb6051694c61d91a988ef1b7aa37fe26149b8b62d5dd
|
|
BLAKE2b-256 checksum How to use checksums |
1bccdeb782de2bf3729a669bc9467b971274aa6b04a94fb295602d3b5957d05c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Aug 7, 2026.
Transparency logRelease files / mem_report-0.1.0-py3-none-any.whl
| Download URL | mem_report-0.1.0-py3-none-any.whl |
|---|---|
| Size | 27.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
449ee8d0b9df3dc41cb1326fda3c2c5ef42e2e8fb3edd0ec0d3599ce102106e8
|
|
BLAKE2b-256 checksum How to use checksums |
129d2e9983d41ca4443d3da586f89ee9946577a6a3ecd5fe19dc5b68da5a1d19
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Aug 7, 2026.
Transparency log