Skip to main content

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   # or: pip 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:

  1. it is orphaned (PPID 1) — its spawning session is gone
  2. it has no controlling terminal
  3. it is older than --min-age (default 60s, so processes still starting up are never touched)
  4. it is owned by you (or you are root)
  5. it is neither the running memr process nor any ancestor of it
  6. 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

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
--top N report 10 rows to show for --all
--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
--max-bytes BYTES watch 10485760 rotate the log above this size
--verbose watch off report each sample on stderr
--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

memr grew out of two bash scripts, claude-mem-report and claude-memwatch, which sampled per-process RSS for Claude Code only. The first is where the process-tree attribution fix started, and that fix is what motivated this rewrite. Both are superseded and no longer shipped — they remain in the git history.

License

MIT

Metadata

Release files for mem-report 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mem-report 0.1.1
File Size Uploaded
mem_report-0.1.1.tar.gz 50.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for mem-report 0.1.1
File Interpreter ABI Platform
mem_report-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 78.0 kB

Release files / mem_report-0.1.1.tar.gz

Download URL mem_report-0.1.1.tar.gz
Size 50.6 kB
Tags Source
SHA-256 checksum
How to use checksums
68387021190060a985ddb7583fbe560d2286d84f55e868cc9bf12158dea8731c
BLAKE2b-256 checksum
How to use checksums
9b05d7376869da00223bdef2d2d66f8442687a71a72a8b8f805b77ac642e3424
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

Release files / mem_report-0.1.1-py3-none-any.whl

Download URL mem_report-0.1.1-py3-none-any.whl
Size 27.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3b10bac0a232d4d28883fc0d0c7db44b3968031f082a88b98e39cccd6aa2b581
BLAKE2b-256 checksum
How to use checksums
efc7014e7afc68084fb93a1a3244e13c366ad582c2ba950f6ed53ac0594518fd
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

Release history Release notifications | RSS feed

0.1.2

2 release files

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page