Disk reclaim
Every byte is rebuildable, re-downloadable, or irreplaceable.
Reclaim the first freely, the second with a nod, the third never. Sort anything
this skill doesn't name into one of those three and act accordingly.
The biggest number is rarely the right first target: a 60G game is one click to
restore, a 6G photo library is gone forever.
Survey before promising
df -h /System/Volumes/Data for the real figure.
cleanup --dry-run sizes its known cache targets without touching anything
(--json to parse it, cleanup --help for the target list). Do this
before hand-rolling deletion, but do not mistake it for a complete cache
inventory. Give it the login PATH: each target probes for its tool, and a
bare non-interactive shell without mise shims or /opt/homebrew/bin reports
those as not available on this host and silently drops them from the
total.
Then probe ~/Library/Caches as well as the large home directories.
Individual tools use different cache roots - for example, a browser cache
can live there while cleanup checks only ~/.cache.
Only then drill. du -sh ~/* ~/.[!.]* is slow on a large home - background
it, or use dust. A glob over a directory holding tens of thousands of
entries dies with argument list too long; du -d1 -xh <dir> walks it
instead. Scan named roots separately: cloud-managed or protected
trees can stall a broad scan. A stalled path is unclassified, not zero. A
per-dir scan that emits no line for a named directory stalled on it - that
dir is the prime suspect, not empty; re-scan it alone with a timeout. On a
dev machine the source root (~/git, ~/src, ~/code) is routinely the
single largest tree and the classic stall culprit - build outputs and caches
nested inside repos - so size it early and on its own. The one stall that is
not a suspect is ~/Library/Containers (see Gotchas). ncdu is for the
user to drive: it's a TUI and gives an agent nothing non-interactively.
Reconcile du home subtotals against the volume's own used figure before
concluding. df is the wrong denominator for this: on a shared APFS
container it reports container-wide usage - Data volume plus the System
volume, /nix, and snapshots - so every volume prints the same numbers
and none of them is what du ~ can add up to. Read the Data volume alone:
diskutil info /System/Volumes/Data | grep 'Volume Used'
It prints decimal GB; divide by 1.074 to compare with du -h. The gap
is not small - one verified reading had df at 131G used against a Data
volume holding 99.0 GB (92 GiB). Home rarely equals even that: a shortfall
lives outside ~ - probe /private/tmp, /opt/homebrew, /Library
(/nix is its own volume, so it is outside the Data figure entirely).
Until the accounted total approaches Volume Used, the survey is
unfinished: a large unexplained remainder is not a footnote, it is the
reclaim target - keep drilling before presenting any plan.
Quote a reclaim estimate only for things you have actually probed - and that
binds the options you offer as much as the totals you report. Bucket a tree
before proposing a cut-off through it: dir names carry issue numbers, not
dates, and a plausible-sounding "delete everything before July" split of a
10.9G tree once freed 45M. Size the buckets, then offer the split. Sizes on
this machine change; check every time rather than trusting a remembered figure.
Where large things tend to hide: ~/Library/Application Support (games, model
weights), Rust target/ dirs under repos, ~/Downloads, LLM/Whisper model
stores, and /private/tmp (dev/agent scratch accumulates there and is cleared
only on reboot, so a long-uptime Mac hoards tens of GB) - reclaim it by
rebooting or with cleanup --target claude-temp --yes. That opt-in target
removes only current-user Claude session scratch older than 60 minutes. It
protects live sessions from every configured profile and every hibernation
record, including orphaned records whose pane is gone.
What may be deleted
| Class |
Examples |
Rule |
| Rebuildable |
target/, node_modules/, caches |
Proceed |
| Re-downloadable |
Steam games, LLM/Whisper models, media rips |
Name it and the size, proceed on a nod |
| Irreplaceable |
Photos, documents, anything authored |
Never without an explicit yes |
Before classifying a language cache, check its tool still exists:
command -v dart flutter gradle go. A cache whose toolchain is gone is not
"re-downloadable pending a nod", it is dead weight - ~/.pub-cache,
~/.gradle and ~/.dartServer held 1.6G between them on a machine with no
Dart, Flutter or Gradle installed at all. go is the same pair twice over:
~/Library/Caches/go-build and ~/go/pkg/mod (690M and 124M here) are both
dead weight with no go on the machine. cleanup --target go --yes delegates
to go clean -cache -modcache when Go exists. Without Go it removes only
those canonical roots and preserves ~/go/src and ~/go/bin. The check
extends to the manager:
Android's sdkmanager is a JVM wrapper, so with no java it cannot list or
uninstall the packages it installed, and the idiomatic-cleaner route is closed
before you reach it (9.1G of system-images, ndk and emulator sat under a
sdkmanager that could not run). Hand-delete those, leaving the
package-manager-owned cmdline-tools alone. The mirror-image failure is a
manager that runs but has no removal verb: LM Studio's lms has get, load,
ls and import but no rm, so a model comes out only by deleting
~/.lmstudio/models/<publisher>/ once lms ps confirms none are loaded.
Ask before deleting anything in ~/Downloads - it mixes all three classes.
Zip-alongside-extracted-folder pairs are the reliable safe win there; media is
the user's call, however obviously disposable it looks.
Treat an active project's build tree differently from an idle one. It is
rebuildable, but cleaning it during active work is short-lived headroom and
forces an expensive rebuild; prefer other candidates first and make that
trade-off explicit.
Untracked is not disposable: a gitignored .cache/ or output tree can hold
expensive-to-rebuild artefacts or irreplaceable captures, so sort its contents
by the three classes - don't clear it wholesale because git ignores it. A
project cleaner or its docs, where present, is the fastest classifier.
Gotchas
rm -rf is denied by settings. Reach for the idiomatic cleaner instead:
cleanup --target <id> --yes, cargo clean, uv cache clean, pnpm store prune. rm -f on named files is allowed. Bundling several removals into one
command gets the whole command denied, so keep them separate.
- Hibernated agents still own their scratch state. Conversation persistence
does not prove that scratch files are disposable.
cleanup --target claude-temp fails closed if any live-agent query or hibernation record is
unreadable, and refreshes both sets immediately before deletion. Do not
hand-delete /private/tmp/claude-<uid> wholesale.
- Confirm reclaim with
df, not the command's exit code - nor its reported
total. macOS /usr/bin/trash exits non-zero if any path arg is missing
while still trashing the rest, and says nothing about bytes freed; du ~/.Trash reads 0 whether or not anything is in it, because the directory
is TCC-protected rather than empty. Cleaners that do report a
figure report apparent size: cargo clean printed Removed 704697 files, 219.3GiB total for a tree du and df both put at 63 GiB - a 3.5x
overstatement from hardlinks and sparse files. Never pass a cleaner's own
number to the user. After any delete/clean, re-check df (or the target's
du) - that is ground truth, not exit status.
- Trashing frees nothing until the Trash is emptied, on a GUI Mac too. The
bytes leave
du ~ and never reach df, so a session that trashes its way to
a result cannot show the result. An agent can neither read ~/.Trash (TCC)
nor safely empty it - it may hold the user's own items. So for unambiguously
rebuildable targets prefer real deletion, via an idiomatic cleaner,
git worktree remove or wt-remove, precisely so df moves inside the
session. Where trash is right, close by reporting how much is pending in it
and that emptying is the user's action.
trash on a many-file tree takes minutes (it moves, it doesn't unlink),
so it outlives tool timeouts and gets killed mid-move, leaving some args done
and some untouched. Background it, and verify per-path afterwards with ls -d rather than trusting one exit code for the whole list.
- Over SSH, trashing is close to one-way, and frees nothing. A headless
session can neither read
~/.Trash (that needs Full Disk Access) nor drive
Finder (Automation consent cannot be prompted with no GUI), so
osascript -e 'tell application "Finder" to empty trash' times out with
AppleEvent -1712. That code means missing consent, not a wedged Finder:
killall Finder changes nothing, and polling df for it wastes the whole
timeout. Recovery hangs on knowing the exact names, so run trash -v and
keep its Moved "<src>" to "/Users/<you>/.Trash/<name>" lines - the
directory is writable even when unreadable, so mv ~/.Trash/<name> /private/tmp/<name> retrieves an item by name, and it can be deleted there.
Where the deletion is plainly rebuildable and the box is headless, prefer a
cleaner or an in-tmp delete over trash in the first place.
~/Library/Containers stalls every du and is never the answer. It is
hundreds of com.apple.* sandboxes (600 on this machine, all Apple, largest
44K). It is the one directory where a stall means TCC-protected paths, not
size - don't spend three scans on it as the "prime suspect". Confirm cheaply
with ls ~/Library/Containers | grep -vc '^com\.apple\.'; if that is 0,
move on.
- A mounted DMG under
/tmp (cask/.pkg install leftover) reports
Read-only file system and blocks its parent's deletion until
hdiutil detach /dev/diskN (find it via hdiutil info); the leftover
.cdr/dir then clears with rm -f/rmdir.
- Rust build dirs:
cleanup --target cargo-target --yes (opt-in; scans
$CLEANUP_CARGO_ROOTS). cleanup's cargo target is the registry cache
only and does not touch target/. These dirs get large enough to dominate a
survey while .git beside them stays small.
- Agent worktrees under
<repo>/.claude/worktrees/ each carry their own
build tree, so one repo holds several copies of its own target/ (23G across
three here, against 40G in the repo's live one). They read as irreplaceable
because their worktree-agent-* branches hold unmerged commits, but
git worktree remove drops only the checkout - the branch and its commits
stay in the main repo, so a clean one goes with no loss. Check
git status --porcelain in each first, and confirm the branch survives
afterwards. Being inside the repo, they hide from a ~/git/* scan behind the
repo's own total.
- Managed worktrees under
~/.trees are the same trap at ten times the
size: each carries its own node_modules and build output (~2.4G plus ~2.9G
each in a Next.js repo; 25 worktrees held 68G here). wt-status --all --pr --json classifies them and wt-clean --dry-run previews only the MERGED-PR
set. Do not carry the branch-survives advice above across to it: wt-clean
deletes the branch too - it hard-codes --delete-branch, so git branch -d
runs after each removal - and wt-clean --force escalates that to git branch -D, which destroys unmerged commits. For the merged-but-no-PR worktrees
wt-clean deliberately spares, bare wt-remove <path> is the
branch-preserving primitive. To keep every worktree and branch while
reclaiming rebuildable output, use cleanup --target worktree-build --yes.
It protects worktrees containing any live or hibernated agent CWD, removes
only node_modules, .next, .turbo, and coverage, and skips an artefact
if Git reports any tracked file beneath it.
- Several Playwright browser revisions is the normal state, not stale
build-up. A machine with many repos pins one revision per
playwright-core
version, so four chromium revisions in ~/Library/Caches/ms-playwright can
all be live, and pruning the "old" ones forces gigabytes of re-download.
Resolve it rather than guessing by age: each file under <root>/.links names
a playwright-core install, that install's browsers.json lists every
{name, revision} it needs, and the on-disk directory is the name with
hyphens replaced by underscores plus -<revision> (chromium-headless-shell
becomes chromium_headless_shell-1234). Anything outside that set is
unreferenced. cleanup --target playwright walks exactly this; cleanup's
browsers target historically probed ~/.cache only and reported 0 against
3.3G.
- Aube:
~/.cache/aube/virtual-store is a live mise npm-tool working set,
not disposable cache. Do not delete it: it leaves mise tool shims dangling.
aube store prune is the supported way to reclaim unreferenced package data;
its saving may be zero and cannot be estimated from the whole store size.
Since mise embedded aube as a library, a newer install keeps its store
inside the install at <install>/node_modules/.mise rather than in the
shared cache. A sweep finds it as bulk under
~/.local/share/mise/installs/, and it looks like cache because of the name.
It is not: it is the tool's own dependency tree, and deleting it breaks the
tool. Reclaim it with mise prune / uninstalling the tool, never by hand.
- mise downloads:
mise prune can remove an old tool while its downloaded
archives remain under ~/.local/share/mise/downloads/<tool>/. That root is
in mise's data dir, not its cache dir (~/Library/Caches/mise), so
mise cache clear never touches it. For a large entry, confirm no installed
tool still references it (mise ls <tool>) before treating it as
re-downloadable and removing it after a nod. If a legacy
~/.local/share/mise/http-tarballs dir still exists
([ -d ~/.local/share/mise/http-tarballs ]), the same applies there - read
an entry's metadata.json before removing it.
- APFS local snapshots pin deleted blocks. After big deletions the
df
figure can refuse to move: Time Machine local snapshots keep the old blocks
live. Inspect with tmutil listlocalsnapshots /; reclaim with
tmutil thinlocalsnapshots / <bytes> 4, or wait - macOS thins them under
pressure. Never quote purgeable or snapshot space as a saving; it is not
yours to promise.
/nix/store size is not reclaimable space. Most of it is live. Trust
cleanup's nix probe, which sizes dead paths only; nix-collect-garbage -d
routinely frees nothing while costing rollback generations. Never quote the
store total as a saving.
- Podman is separate from Docker. A stopped Podman VM can make Docker
cleanup look empty while
~/.local/share/containers/podman/machine remains
large. Inspect podman machine list; remove a machine only with a nod,
because it discards its images, containers, and volumes.
- VM disks are sparse and never shrink. Even a successful in-guest prune
leaves host
df unmoved: ~/.colima/_lima/_disks/colima/datadisk is 100 GiB
apparent against 38G allocated, and a sparse file only grows, so
docker system prune frees blocks inside the guest filesystem while the host
allocation stays put. Only colima delete (or the Podman equivalent) returns
host bytes, and it discards every image, container and volume - a nod, never
a default. Never quote a VM disk's size as reclaimable; like /nix/store,
the estimate would equal the total.
colima delete also leaves the named data disk behind (colima list goes
empty while _lima/_disks/<name> still holds gigabytes). Check
LIMA_HOME=~/.colima/_lima limactl disk list for an entry with an empty
IN-USE-BY and remove it with limactl disk delete <name>. nix's colima
package doesn't expose limactl on PATH; it lives in the lima-full store
path colima references.
brew cleanup -n under-reports. It applies brew's 120-day policy, so it
can print nothing while gigabytes sit in $(brew --cache)/downloads. Use
cleanup --target brew (or brew cleanup --prune=all -n) for the true
figure.
- Steam: uninstall in the app, never delete
steamapps/common/* - the
manifests desync. Usually the largest single win, and the user must do it.
- Yarn v1: do not probe
yarn cache clean with --help - it runs the
cleaner. Use yarn cache --help to inspect the parent command instead.
- Don't pipe a long-running background command through
tail: the output is
lost to buffering and you end up polling for a result that never lands.
1---2name: disk-reclaim3description: Finds what is consuming disk space on this Mac and reclaims it safely. Use when the disk is low or full, or the user asks to analyse disc usage, find what's eating space, free up space, or clean up their Mac - including mentions of ncdu, du, dust, or "startup disk full". Not for pruning a single project's build output, or for disk on a remote server.4---5
6# Disk reclaim
7
8Every byte is **rebuildable**, **re-downloadable**, or **irreplaceable**.
9Reclaim the first freely, the second with a nod, the third never. Sort anything
10this skill doesn't name into one of those three and act accordingly.
11
12The biggest number is rarely the right first target: a 60G game is one click to
13restore, a 6G photo library is gone forever.
14
15## Survey before promising
16
171. `df -h /System/Volumes/Data` for the real figure.
182. `cleanup --dry-run` sizes its known cache targets without touching anything
19 (`--json` to parse it, `cleanup --help` for the target list). Do this
20 *before* hand-rolling deletion, but do not mistake it for a complete cache
21 inventory. Give it the login PATH: each target probes for its tool, and a
22 bare non-interactive shell without mise shims or `/opt/homebrew/bin` reports
23 those as `not available on this host` and silently drops them from the
24 total.
253. Then probe `~/Library/Caches` as well as the large home directories.
26 Individual tools use different cache roots - for example, a browser cache
27 can live there while `cleanup` checks only `~/.cache`.
284. Only then drill. `du -sh ~/* ~/.[!.]*` is slow on a large home - background
29 it, or use `dust`. A glob over a directory holding tens of thousands of
30 entries dies with `argument list too long`; `du -d1 -xh <dir>` walks it
31 instead. Scan named roots separately: cloud-managed or protected
32 trees can stall a broad scan. A stalled path is unclassified, not zero. A
33 per-dir scan that emits *no line* for a named directory stalled on it - that
34 dir is the prime suspect, not empty; re-scan it alone with a timeout. On a
35 dev machine the source root (`~/git`, `~/src`, `~/code`) is routinely the
36 single largest tree and the classic stall culprit - build outputs and caches
37 nested inside repos - so size it early and on its own. The one stall that is
38 *not* a suspect is `~/Library/Containers` (see Gotchas). `ncdu` is for the
39 user to drive: it's a TUI and gives an agent nothing non-interactively.
405. Reconcile `du` home subtotals against the volume's *own* used figure before
41 concluding. **`df` is the wrong denominator for this**: on a shared APFS
42 container it reports container-wide usage - Data volume plus the System
43 volume, `/nix`, and snapshots - so every volume prints the *same* numbers
44 and none of them is what `du ~` can add up to. Read the Data volume alone:
45
46 ```bash
47 diskutil info /System/Volumes/Data | grep 'Volume Used'
48 ```
49
50 It prints **decimal GB**; divide by 1.074 to compare with `du -h`. The gap
51 is not small - one verified reading had `df` at 131G used against a Data
52 volume holding 99.0 GB (92 GiB). Home rarely equals even that: a shortfall
53 lives outside `~` - probe `/private/tmp`, `/opt/homebrew`, `/Library`
54 (`/nix` is its own volume, so it is outside the Data figure entirely).
55 Until the accounted total approaches *Volume Used*, the survey is
56 unfinished: a large unexplained remainder is not a footnote, it is the
57 reclaim target - keep drilling before presenting any plan.
58
59Quote a reclaim estimate only for things you have actually probed - and that
60binds the *options you offer* as much as the totals you report. Bucket a tree
61before proposing a cut-off through it: dir names carry issue numbers, not
62dates, and a plausible-sounding "delete everything before July" split of a
6310.9G tree once freed 45M. Size the buckets, then offer the split. Sizes on
64this machine change; check every time rather than trusting a remembered figure.
65
66Where large things tend to hide: `~/Library/Application Support` (games, model
67weights), Rust `target/` dirs under repos, `~/Downloads`, LLM/Whisper model
68stores, and `/private/tmp` (dev/agent scratch accumulates there and is cleared
69only on reboot, so a long-uptime Mac hoards tens of GB) - reclaim it by
70rebooting or with `cleanup --target claude-temp --yes`. That opt-in target
71removes only current-user Claude session scratch older than 60 minutes. It
72protects live sessions from every configured profile and every hibernation
73record, including orphaned records whose pane is gone.
74
75## What may be deleted
76
77| Class | Examples | Rule |
78|---|---|---|
79| Rebuildable | `target/`, `node_modules/`, caches | Proceed |
80| Re-downloadable | Steam games, LLM/Whisper models, media rips | Name it and the size, proceed on a nod |
81| Irreplaceable | Photos, documents, anything authored | Never without an explicit yes |
82
83Before classifying a language cache, check its tool still exists:
84`command -v dart flutter gradle go`. A cache whose toolchain is gone is not
85"re-downloadable pending a nod", it is dead weight - `~/.pub-cache`,
86`~/.gradle` and `~/.dartServer` held 1.6G between them on a machine with no
87Dart, Flutter or Gradle installed at all. `go` is the same pair twice over:
88`~/Library/Caches/go-build` and `~/go/pkg/mod` (690M and 124M here) are both
89dead weight with no `go` on the machine. `cleanup --target go --yes` delegates
90to `go clean -cache -modcache` when Go exists. Without Go it removes only
91those canonical roots and preserves `~/go/src` and `~/go/bin`. The check
92extends to the *manager*:
93Android's `sdkmanager` is a JVM wrapper, so with no `java` it cannot list or
94uninstall the packages it installed, and the idiomatic-cleaner route is closed
95before you reach it (9.1G of `system-images`, `ndk` and `emulator` sat under a
96`sdkmanager` that could not run). Hand-delete those, leaving the
97package-manager-owned `cmdline-tools` alone. The mirror-image failure is a
98manager that runs but has no removal verb: LM Studio's `lms` has `get`, `load`,
99`ls` and `import` but no `rm`, so a model comes out only by deleting
100`~/.lmstudio/models/<publisher>/` once `lms ps` confirms none are loaded.
101
102**Ask before deleting anything in `~/Downloads`** - it mixes all three classes.
103Zip-alongside-extracted-folder pairs are the reliable safe win there; media is
104the user's call, however obviously disposable it looks.
105
106Treat an active project's build tree differently from an idle one. It is
107rebuildable, but cleaning it during active work is short-lived headroom and
108forces an expensive rebuild; prefer other candidates first and make that
109trade-off explicit.
110
111Untracked is not disposable: a gitignored `.cache/` or output tree can hold
112expensive-to-rebuild artefacts or irreplaceable captures, so sort its contents
113by the three classes - don't clear it wholesale because git ignores it. A
114project cleaner or its docs, where present, is the fastest classifier.
115
116## Gotchas
117
118- **`rm -rf` is denied by settings.** Reach for the idiomatic cleaner instead:
119 `cleanup --target <id> --yes`, `cargo clean`, `uv cache clean`, `pnpm store
120 prune`. `rm -f` on named files is allowed. Bundling several removals into one
121 command gets the whole command denied, so keep them separate.
122- **Hibernated agents still own their scratch state.** Conversation persistence
123 does not prove that scratch files are disposable. `cleanup --target
124 claude-temp` fails closed if any live-agent query or hibernation record is
125 unreadable, and refreshes both sets immediately before deletion. Do not
126 hand-delete `/private/tmp/claude-<uid>` wholesale.
127- **Confirm reclaim with `df`, not the command's exit code - nor its reported
128 total.** macOS `/usr/bin/trash` exits non-zero if *any* path arg is missing
129 while still trashing the rest, and says nothing about bytes freed; `du
130 ~/.Trash` reads `0` whether or not anything is in it, because the directory
131 is TCC-protected rather than empty. Cleaners that *do* report a
132 figure report **apparent** size: `cargo clean` printed `Removed 704697 files,
133 219.3GiB total` for a tree `du` and `df` both put at 63 GiB - a 3.5x
134 overstatement from hardlinks and sparse files. Never pass a cleaner's own
135 number to the user. After any delete/clean, re-check `df` (or the target's
136 `du`) - that is ground truth, not exit status.
137- **Trashing frees nothing until the Trash is emptied, on a GUI Mac too.** The
138 bytes leave `du ~` and never reach `df`, so a session that trashes its way to
139 a result cannot show the result. An agent can neither read `~/.Trash` (TCC)
140 nor safely empty it - it may hold the user's own items. So for unambiguously
141 rebuildable targets prefer real deletion, via an idiomatic cleaner,
142 `git worktree remove` or `wt-remove`, precisely so `df` moves inside the
143 session. Where `trash` is right, close by reporting how much is pending in it
144 and that emptying is the user's action.
145- **`trash` on a many-file tree takes minutes** (it moves, it doesn't unlink),
146 so it outlives tool timeouts and gets killed mid-move, leaving some args done
147 and some untouched. Background it, and verify per-path afterwards with `ls
148 -d` rather than trusting one exit code for the whole list.
149- **Over SSH, trashing is close to one-way, and frees nothing.** A headless
150 session can neither read `~/.Trash` (that needs Full Disk Access) nor drive
151 Finder (Automation consent cannot be prompted with no GUI), so
152 `osascript -e 'tell application "Finder" to empty trash'` times out with
153 AppleEvent `-1712`. That code means missing consent, not a wedged Finder:
154 `killall Finder` changes nothing, and polling `df` for it wastes the whole
155 timeout. Recovery hangs on knowing the exact names, so run `trash -v` and
156 keep its `Moved "<src>" to "/Users/<you>/.Trash/<name>"` lines - the
157 directory is writable even when unreadable, so `mv ~/.Trash/<name>
158 /private/tmp/<name>` retrieves an item by name, and it can be deleted there.
159 Where the deletion is plainly rebuildable and the box is headless, prefer a
160 cleaner or an in-tmp delete over `trash` in the first place.
161- **`~/Library/Containers` stalls every `du` and is never the answer.** It is
162 hundreds of `com.apple.*` sandboxes (600 on this machine, all Apple, largest
163 44K). It is the one directory where a stall means TCC-protected paths, not
164 size - don't spend three scans on it as the "prime suspect". Confirm cheaply
165 with `ls ~/Library/Containers | grep -vc '^com\.apple\.'`; if that is 0,
166 move on.
167- **A mounted DMG under `/tmp`** (cask/`.pkg` install leftover) reports
168 `Read-only file system` and blocks its parent's deletion until
169 `hdiutil detach /dev/diskN` (find it via `hdiutil info`); the leftover
170 `.cdr`/dir then clears with `rm -f`/`rmdir`.
171- **Rust build dirs: `cleanup --target cargo-target --yes`** (opt-in; scans
172 `$CLEANUP_CARGO_ROOTS`). `cleanup`'s `cargo` target is the registry cache
173 only and does not touch `target/`. These dirs get large enough to dominate a
174 survey while `.git` beside them stays small.
175- **Agent worktrees under `<repo>/.claude/worktrees/`** each carry their own
176 build tree, so one repo holds several copies of its own `target/` (23G across
177 three here, against 40G in the repo's live one). They read as irreplaceable
178 because their `worktree-agent-*` branches hold unmerged commits, but
179 `git worktree remove` drops only the checkout - the branch and its commits
180 stay in the main repo, so a clean one goes with no loss. Check
181 `git status --porcelain` in each first, and confirm the branch survives
182 afterwards. Being inside the repo, they hide from a `~/git/*` scan behind the
183 repo's own total.
184- **Managed worktrees under `~/.trees`** are the same trap at ten times the
185 size: each carries its own `node_modules` and build output (~2.4G plus ~2.9G
186 each in a Next.js repo; 25 worktrees held 68G here). `wt-status --all --pr
187 --json` classifies them and `wt-clean --dry-run` previews only the MERGED-PR
188 set. Do not carry the branch-survives advice above across to it: **`wt-clean`
189 deletes the branch too** - it hard-codes `--delete-branch`, so `git branch -d`
190 runs after each removal - and `wt-clean --force` escalates that to `git branch
191 -D`, which destroys unmerged commits. For the merged-but-no-PR worktrees
192 `wt-clean` deliberately spares, bare `wt-remove <path>` is the
193 branch-preserving primitive. To keep every worktree and branch while
194 reclaiming rebuildable output, use `cleanup --target worktree-build --yes`.
195 It protects worktrees containing any live or hibernated agent CWD, removes
196 only `node_modules`, `.next`, `.turbo`, and `coverage`, and skips an artefact
197 if Git reports any tracked file beneath it.
198- **Several Playwright browser revisions is the normal state, not stale
199 build-up.** A machine with many repos pins one revision per `playwright-core`
200 version, so four chromium revisions in `~/Library/Caches/ms-playwright` can
201 all be live, and pruning the "old" ones forces gigabytes of re-download.
202 Resolve it rather than guessing by age: each file under `<root>/.links` names
203 a `playwright-core` install, that install's `browsers.json` lists every
204 `{name, revision}` it needs, and the on-disk directory is the name with
205 hyphens replaced by underscores plus `-<revision>` (`chromium-headless-shell`
206 becomes `chromium_headless_shell-1234`). Anything outside that set is
207 unreferenced. `cleanup --target playwright` walks exactly this; `cleanup`'s
208 `browsers` target historically probed `~/.cache` only and reported 0 against
209 3.3G.
210- **Aube:** `~/.cache/aube/virtual-store` is a live mise npm-tool working set,
211 not disposable cache. Do not delete it: it leaves mise tool shims dangling.
212 `aube store prune` is the supported way to reclaim unreferenced package data;
213 its saving may be zero and cannot be estimated from the whole store size.
214 Since mise embedded aube as a library, a newer install keeps its store
215 **inside the install** at `<install>/node_modules/.mise` rather than in the
216 shared cache. A sweep finds it as bulk under
217 `~/.local/share/mise/installs/`, and it looks like cache because of the name.
218 It is not: it is the tool's own dependency tree, and deleting it breaks the
219 tool. Reclaim it with `mise prune` / uninstalling the tool, never by hand.
220- **mise downloads:** `mise prune` can remove an old tool while its downloaded
221 archives remain under `~/.local/share/mise/downloads/<tool>/`. That root is
222 in mise's *data* dir, not its cache dir (`~/Library/Caches/mise`), so
223 `mise cache clear` never touches it. For a large entry, confirm no installed
224 tool still references it (`mise ls <tool>`) before treating it as
225 re-downloadable and removing it after a nod. If a legacy
226 `~/.local/share/mise/http-tarballs` dir still exists
227 (`[ -d ~/.local/share/mise/http-tarballs ]`), the same applies there - read
228 an entry's `metadata.json` before removing it.
229- **APFS local snapshots pin deleted blocks.** After big deletions the `df`
230 figure can refuse to move: Time Machine local snapshots keep the old blocks
231 live. Inspect with `tmutil listlocalsnapshots /`; reclaim with
232 `tmutil thinlocalsnapshots / <bytes> 4`, or wait - macOS thins them under
233 pressure. Never quote purgeable or snapshot space as a saving; it is not
234 yours to promise.
235- **`/nix/store` size is not reclaimable space.** Most of it is live. Trust
236 `cleanup`'s nix probe, which sizes dead paths only; `nix-collect-garbage -d`
237 routinely frees nothing while costing rollback generations. Never quote the
238 store total as a saving.
239- **Podman is separate from Docker.** A stopped Podman VM can make Docker
240 cleanup look empty while `~/.local/share/containers/podman/machine` remains
241 large. Inspect `podman machine list`; remove a machine only with a nod,
242 because it discards its images, containers, and volumes.
243- **VM disks are sparse and never shrink.** Even a *successful* in-guest prune
244 leaves host `df` unmoved: `~/.colima/_lima/_disks/colima/datadisk` is 100 GiB
245 apparent against 38G allocated, and a sparse file only grows, so
246 `docker system prune` frees blocks inside the guest filesystem while the host
247 allocation stays put. Only `colima delete` (or the Podman equivalent) returns
248 host bytes, and it discards every image, container and volume - a nod, never
249 a default. Never quote a VM disk's size as reclaimable; like `/nix/store`,
250 the estimate would equal the total.
251 `colima delete` also leaves the *named data disk* behind (`colima list` goes
252 empty while `_lima/_disks/<name>` still holds gigabytes). Check
253 `LIMA_HOME=~/.colima/_lima limactl disk list` for an entry with an empty
254 `IN-USE-BY` and remove it with `limactl disk delete <name>`. nix's colima
255 package doesn't expose `limactl` on PATH; it lives in the `lima-full` store
256 path colima references.
257- **`brew cleanup -n` under-reports.** It applies brew's 120-day policy, so it
258 can print nothing while gigabytes sit in `$(brew --cache)/downloads`. Use
259 `cleanup --target brew` (or `brew cleanup --prune=all -n`) for the true
260 figure.
261- **Steam: uninstall in the app**, never delete `steamapps/common/*` - the
262 manifests desync. Usually the largest single win, and the user must do it.
263- **Yarn v1:** do not probe `yarn cache clean` with `--help` - it runs the
264 cleaner. Use `yarn cache --help` to inspect the parent command instead.
265- Don't pipe a long-running background command through `tail`: the output is
266 lost to buffering and you end up polling for a result that never lands.