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