Reclaim disk space from caches and build artifacts, with a risk tier on every finding and guards that make it hard to delete something you wanted.
Nothing is deleted without a rule, and every rule carries a tier and a
"here's how you get it back" note. Build artifacts must prove what they are —
a target/ is only a Rust build if Cargo.toml sits beside it; ambiguous names
like dist/ are claimed only when git already ignores them. Dry-run is the
default.
LOW RISK pure caches, regenerate automatically [5.0G]
--------------------------------------------------------------------------
1.2G pnpm-store pnpm content store
1.2G x17 app-cache app HTTP cache
800.1M vscode-vsix VS Code extension installers
595.3M cargo-registry cargo crate sources + archives
MEDIUM RISK reinstallable, costs you time/bandwidth [3.2G]
--------------------------------------------------------------------------
701.3M nuget-packages NuGet global packages
255.2M x7 node-modules installed npm packages
HIGH RISK stateful or expensive - review each one [1.8G]
--------------------------------------------------------------------------
1.4G rustup-toolchains rust toolchains
! prefer `rustup toolchain uninstall <old>` to keep the active one
Desktop app — downloads the latest release
into /Applications, ready to open:
curl -fsSL https://raw.githubusercontent.com/samreshan/cachereaper/main/install.sh -o install.sh
bash install.shCLI — one file, no dependencies, Python 3.9+, macOS and Linux:
git clone https://github.com/samreshan/cachereaper
install -m 755 cachereaper/cachereaper.py ~/.local/bin/cachereaperDownloaded the .dmg by hand and macOS says it is damaged?
It isn't. The app is signed ad-hoc rather than with an Apple Developer ID (that needs a paid membership), so Gatekeeper can't attribute it to anyone and says "damaged" when it means unidentified. On macOS 15+ there is no right-click → Open for this any more. After dragging it to Applications:
xattr -dr com.apple.quarantine /Applications/cachereaper.appThat clears the flag macOS puts on browser downloads — the same decision the
Privacy & Security pane asks for, made up front. install.sh does it for you.
Building from source with ./gui/release.sh avoids the question entirely.
cachereaper # scan, low-risk only (default)
cachereaper scan --tier high -v # everything, with individual paths
cachereaper select # pick what to remove, interactively
cachereaper clean --tier low # delete safe caches (confirms first)
cachereaper clean --tier medium --dry-run # rehearse it
cachereaper tools # safer vendor commands + all rulesselect opens a picker where low-risk groups start ticked and medium and high
start empty, so pressing d immediately does the conservative thing.
| flag | effect |
|---|---|
--tier low|medium|high |
highest tier to include (default low) |
--stale-days N |
only things untouched for N days |
--min-size 10M |
ignore anything smaller |
--only / --exclude RULE... |
filter by rule id |
--roots DIR... |
where to hunt for project artifacts (default $HOME) |
--system |
also /Library/Caches, /private/var/folders (sudo to delete) |
--json |
machine-readable output |
A treemap with the risk tiers painted on top. GrandPerspective shows you what is big; cachereaper shows you what is big and safe to delete.
It opens by asking what to look at rather than walking your disk uninvited, and
holds the window with a live count while it scans. Scan folder… repoints it
later — an external drive, one project, ~/Library.
| Scan folder… | choose a different root and rescan |
| click | drill into a folder |
| backspace / ↑ | go back up |
Select mode (or s) |
click blocks, or drag a box to take many |
| ⌥click in Select | drill in instead of selecting |
| ⌘click in Explore | select the nearest claimed folder |
| esc | clear the selection |
Colour carries one meaning: tiers stay saturated, anything unclaimed drains to
grey so it recedes. Dragging a box across a node_modules means that folder,
not those 400 files.
| tier | meaning | examples |
|---|---|---|
| low | regenerates itself, costs you nothing | npm/pip/cargo caches, Electron app caches, __pycache__, DerivedData |
| medium | reinstallable, costs time or bandwidth | node_modules, virtualenvs, NuGet/Maven caches, git-ignored dist/ |
| high | stateful or expensive — review each | rustup toolchains, simulator devices, Xcode archives |
~55 known cache locations: npm, yarn, pnpm, bun, pip, uv, poetry, cargo, go,
gradle, maven, NuGet, composer, CocoaPods, SwiftPM, deno, Homebrew, Playwright,
Puppeteer, Prisma, Xcode DerivedData and archives, CoreSimulator, VS Code and
Electron app caches, and Library/Caches/*.
Plus project build artifacts, each gated on a marker so a directory is never claimed on its name alone:
| directory | claimed when |
|---|---|
target/ |
Cargo.toml or pom.xml is a sibling |
node_modules/ |
always (unambiguous) |
.venv/, venv/, env/ |
contains pyvenv.cfg |
Pods/ · vendor/ |
Podfile · composer.json is a sibling |
.next/, .turbo/, .vite/, .svelte-kit/, … |
package.json is a sibling |
.gradle/ · .dart_tool/ · _build/ |
build.gradle* · pubspec.yaml · mix.exs |
__pycache__/, .pytest_cache/, .tox/, .terraform/, … |
by name |
build/, dist/, out/, obj/ |
only if git already ignores them |
- Confined to
$HOMEplus roots you pass explicitly.--systemneeds root. - Hard-blocked components, re-checked before every delete:
.git,.ssh,.gnupg,Keychains,Mobile Documents,*.photoslibrary, and anything under OneDrive / Google Drive / Dropbox / iCloud / Nextcloud. - Never follows symlinks or crosses filesystems.
- Re-validated at delete time — a path that changed since the scan is skipped, not deleted.
- Stateful data is not a rule at all: VM disks, chat history,
Downloads, and source directories are never offered. - Everything is logged to
~/.cachereaper/reap-<timestamp>.jsonlwith the path, rule, bytes, and restore command. - High risk requires typing a phrase, not just
y.
Where a vendor command is safer than rm -rf — Docker, Colima, rustup, simctl,
Time Machine snapshots — cachereaper tools prints the command instead.
The rule table lives in cachereaper.py and nowhere else; dump-rules
generates the copy the Rust core embeds, and CI fails if it drifts. The guards
exist in both languages, held honest by shared vectors both suites assert
against.
./gui/dev.sh ~/Programming # map in a browser, no desktop build
./gui/release.sh # universal .app + .dmg
python3 -m unittest discover -s tests # CLI
cargo test --manifest-path gui/core/Cargo.toml # scanner, guards, deletion
node gui/tests/treemap.test.mjs # treemap layoutNew rules are the most useful contribution. A rule needs an id, a tier, a label,
and an honest regen string. If the name is ambiguous, gate it behind a marker
or need_gitignored=True rather than claiming it outright.
MIT