spoilheap

spoilheap docs

spoilheap lists what Codex, Claude Code, OpenCode, Gemini CLI and Cline left on your disk, gives every item a verdict (SAFE, REVIEW, KEEP) with the reason, clears only what you ask for, and can block an agent before the disk fills. Running spoilheap on its own never changes anything.

01Install

curl -fsSL https://blackman99.github.io/spoilheap/install.sh | sh

The script picks the release binary for your OS and CPU, checks it against checksums.txt, and puts it in ~/.local/bin. Set SPOILHEAP_BIN=/usr/local/bin to put it somewhere else, or SPOILHEAP_VERSION=v0.1.0 to pin a version.

From source (Rust 1.85 or newer): cargo install --git https://github.com/Blackman99/spoilheap

By hand: download the archive for your platform from Releases. Linux x86_64 and aarch64 builds are static (musl). macOS builds are for arm64 and x86_64; Windows for x86_64 and arm64. Every archive is listed in checksums.txt on the release.

02First run

spoilheap                 # look. Deletes nothing.
spoilheap clean --safe    # print the plan for every SAFE item
spoilheap clean --safe --yes
spoilheap clean 101 104   # REVIEW items, by number, after you have read them

Every item gets a number from 101 up, like a context number in a dig. The numbers are saved, so spoilheap clean 104 means the item you just saw as [104], even if you open a new terminal. If the item has changed or gone since then, spoilheap says so instead of guessing.

Repositories are searched under the current folder and under ~/code ~/src ~/projects ~/dev ~/work ~/Developer ~/repos when those exist, four levels deep. Point it elsewhere with --root DIR (repeatable) and --depth N.

03How verdicts are made

ItemVerdictRule
Crash dumps (Codex, ChatGPT, Claude, Cursor desktop apps)SAFEAlways. If 30 or more appeared in the last hour, the reason says the app is crash-looping and to quit it first.
Agent worktree, clean, every commit reachable from another branch, tag or remote, untouched for 1 hSAFERemoved with git worktree remove.
Agent worktree with commits found nowhere elseREVIEWgit rev-list --count HEAD --not --exclude=<branch> --branches --tags --remotes; the reason gives the count.
Agent worktree a file changed in within the last hourREVIEWAn agent may still be working in it.
Folder that looks like a worktree but git no longer lists itREVIEWIts repo was moved or pruned, or it is a copy. Removed as a plain folder only if you name it.
Agent worktree with uncommitted changesKEEPgit status --porcelain is not empty.
Agent worktree a running process is sitting inKEEPSome process has its working directory inside it (/proc/*/cwd on Linux, lsof on macOS).
target/, node_modules/, .venv/ … inside an agent worktreeSAFEOnly when git says the folder is ignored and tracks no file in it. Listed apart from the worktree, so a KEEP worktree can still lose its build output.
tmp_pack_*, tmp_idx_*, .tmp-* in .git/objects/packSAFEUntouched for 1 h (a git that is still writing gets left alone).
Stale worktree registrationsSAFEgit worktree prune.
Session history, transcripts, file history, snapshots, task foldersREVIEW / KEEPOlder than --older (default 30 d) is REVIEW, newer is KEEP.
Logs, debug logs, shell snapshotsSAFEOlder than 1 day.
SQLite database (OpenCode, Codex logs)REVIEW / KEEPFree pages are read from the file header (freelist count × page size). REVIEW when at least 500 MB is free, or 20 % and 50 MB; it prints the VACUUM command and the free space VACUUM needs. Never edited by spoilheap.

All git commands run with GIT_OPTIONAL_LOCKS=0, so spoilheap never takes index.lock from an agent that is working.

04clean

spoilheap clean --safe [--yes]
spoilheap clean 104 107 [--yes]
spoilheap clean --safe 113 --yes      # every SAFE item plus REVIEW item 113
  • Without --yes it prints the plan and the total, and deletes nothing.
  • KEEP items are skipped even when named, and the reason is shown.
  • Worktrees are re-checked right before removal, then removed with git worktree remove (no --force). If git refuses, that item fails and the rest go on; the exit code is 1.
  • Files are deleted only inside the folder the item came from; empty parent folders are tidied up to that folder and no further.
  • Databases print a command for you to run when the app is closed, for example sqlite3 '~/.local/share/opencode/opencode.db' 'VACUUM;'.
  • After a real run it measures free space again. If the disk got back less than 70 % of the plan, it says why that usually happens (a process still holds deleted files open, or a filesystem snapshot keeps the blocks).

05watch

spoilheap watch [--every 10s] [--rate 1GB] [--floor 10GB]

Measures the agent folders and every repo's .git/objects/pack on each tick. A location that grows faster than --rate per minute is printed as RUNAWAY with its newest file and the process chain that has files open there (from /proc/*/fd on Linux, lsof on macOS), once per change. Free space under --floor is reported at most once a minute. A quiet line with the free space is printed every minute when nothing moves.

06guard and hooks

spoilheap guard [--floor 10GB | --floor 5%] [--path DIR]
spoilheap hook claude     # prints the settings.json block
spoilheap hook codex      # prints the config.toml block

guard exits 0 when free space is at or above the floor on the home folder and on the folder the agent is working in (read from the hook's JSON on stdin, field cwd, or --path). Under the floor it exits 2 and writes a message to stderr naming the three biggest SAFE items and the command to clear them. Claude Code and Codex both treat exit code 2 from a PreToolUse hook as "block this tool call and show the message to the model".

Codex hooks are behind a feature flag; the printed block sets [features] hooks = true. Codex also reads ~/.codex/hooks.json if you prefer JSON.

07MCP server

claude mcp add spoilheap -- spoilheap mcp
codex mcp add spoilheap -- spoilheap mcp
{ "mcpServers": { "spoilheap": { "command": "spoilheap", "args": ["mcp"] } } }

Stdio JSON-RPC, protocol 2025-06-18. Two tools, both marked read-only:

  • disk_status { path?, floor? }: free and total bytes for the filesystem holding path (default: home), and whether it is under floor (default 10GB).
  • disk_leftovers { min_bytes? }: the same report as spoilheap --json, with verdicts and reasons.

There is no delete tool. The agent can tell you what to run; you run it.

08Paths it reads

AgentWhere
Codex$CODEX_HOME (default ~/.codex): worktrees/, sessions/, archived_sessions/, log/, .tmp/, logs_*.sqlite. Crash dumps from the desktop apps (Codex, ChatGPT, Claude, Cursor) in each app's data folder: ~/Library/Application Support/<App>/Crashpad on macOS, %APPDATA%\<App>\Crashpad on Windows, ~/.config/<App>/Crashpad on Linux. Old bun-installed builds in ~/.bun/install/cache/@openai.
Claude Code$CLAUDE_CONFIG_DIR (default ~/.claude): projects/, file-history/, shell-snapshots/, debug/; <repo>/.claude/worktrees.
OpenCode$XDG_DATA_HOME/opencode (default ~/.local/share/opencode): opencode*.db (+ WAL) or $OPENCODE_DB, snapshot/<project>, storage/, log/.
Gemini CLI~/.gemini/tmp/<project hash>.
ClineglobalStorage/saoudrizwan.claude-dev/{tasks,checkpoints} under VS Code, Cursor, Windsurf and VSCodium user folders, and ~/.cline/data.
Aider<repo>/.aider.tags.cache.v4.

spoilheap paths prints each one with found or absent for your machine. Sizes are allocated blocks; hard links are counted once; symlinks are never followed; scans stay on one filesystem.

09Options and environment

FlagMeaning
--root DIRLook for repositories here (repeatable).
--depth NHow deep under each root (default 4).
--older DHistory older than this is REVIEW (default 30d; units s m h d w).
--allAlso list items under 1 MB.
--jsonMachine-readable output.
--home DIRTreat DIR as the home folder.
--no-colorPlain output.
VariableEffect
CODEX_HOME, CLAUDE_CONFIG_DIR, XDG_DATA_HOME, OPENCODE_DBSame meaning as for the agents themselves.
XDG_CACHE_HOMEWhere the last run's item numbers are kept (spoilheap/last.json; ~/Library/Caches on macOS, %LOCALAPPDATA% on Windows).
NO_COLOR, FORCE_COLOR, COLUMNSOutput styling and width.
SPOILHEAP_VACUUM_MINLower the "worth a VACUUM" bar (default 50MB, with 20 %).
SPOILHEAP_FAKE_FREE=3GB/500GBPretend this much is free (for testing guard and hooks).
SPOILHEAP_OS, SPOILHEAP_HOME, SPOILHEAP_APPARENT_SIZETest knobs: use another OS's path layout, another home, or count file length instead of blocks (for sparse demo files).

10JSON output

{
  "tool": "spoilheap", "version": "0.1.0", "generated_at": "2026-10-19T02:41:07Z",
  "disks": [{ "path": "/", "total_bytes": 494000000000, "free_bytes": 3100000000 }],
  "totals": { "bytes": 117074251949, "safe_reclaimable_bytes": 56524587087,
              "review_reclaimable_bytes": 54193328174, "keep_bytes": 6186614832 },
  "repos_scanned": 1,
  "items": [{
    "id": 127, "harness": "repo", "kind": "tmp-pack",
    "label": "payments: 6 half-written git packs",
    "path": "/Users/you/code/payments/.git/objects/pack",
    "bytes": 11952000000, "reclaimable_bytes": 11952000000, "files": 6,
    "newest_age_s": 172800, "oldest_age_s": 172800,
    "verdict": "SAFE",
    "reason": "tmp_pack_* from a git that died mid-write (openai/codex#54955); untouched for 1 h+",
    "action": { "type": "remove", "paths": 6 }, "parent": null
  }],
  "notes": []
}

action.type is one of remove, git-worktree-remove, git-worktree-prune, manual (with command) or none. parent is the id of the worktree a build-output item sits in.

11Exit codes

0Done (for guard: enough free space).
1clean: at least one item could not be removed.
2guard: under the floor; the hook blocks the tool call.
64Usage error: unknown flag or command, bad number. The message suggests the nearest flag.

12FAQ

Is it safe to run?

spoilheap, paths, watch, guard and mcp only read. Only clean … --yes deletes, and only items you selected, inside the folders they came from.

Why does the disk not get back everything clean promised?

A running app may hold deleted files open (the space comes back when it quits), or a filesystem snapshot (APFS local snapshots, Btrfs, ZFS) may keep the blocks. spoilheap measures before and after and says when this happened.

Why does it not delete old chats by itself?

Because you might want them: codex resume, Claude Code's /resume and Cline's task history all read them. They are REVIEW so you decide, with the age and size in front of you.

Why not just run VACUUM for me?

VACUUM rewrites the file and needs roughly its live size in free space while it runs. On a nearly full disk, with the agent possibly running, that is how databases get corrupted. You run it when the app is closed.

Does it know about my other agent?

If it keeps data under its own folder, open an issue with the path and a link to the source or docs that define it. Paths are only added from the agent's own source or docs.

Does it send anything anywhere?

No. No network, no telemetry. The only file it writes outside clean is the id map in the cache folder.