- name
- macos-disk-usage
- description
- macOS disk-usage forensics and space recovery on APFS. Use when df reports a disk near-full, hunting what's eating space, reclaiming OrbStack/Docker, or pruning caches and local snapshots.
- user-invocable
- false
- allowed-tools
- Bash(bash *), Bash(uname *), Bash(df *), Bash(du *), Bash(find *), Bash(diskutil *), Bash(tmutil *), Bash(docker *), Bash(brew *), Bash(go *), Bash(pip *), Bash(uv *), Bash(dust *), Bash(dua *), Bash(gdu *), Bash(ncdu *), Bash(mise *), Bash(git *), Read, Grep, Glob, TodoWrite
- created
- 2026-06-21T00:00:00.000Z
- modified
- 2026-10-13T00:00:00.000Z
- reviewed
- 2026-08-04T00:00:00.000Z
# macOS Disk Usage & Space Recovery
## When to Use This Skill
| Use this skill when... | Use something else when... |
|------------------------|----------------------------|
| `df` reports a volume near-full and you need the honest figure | The root disk on Linux is full — use the Linux-focused `disk-full-recovery` rule |
| Hunting "what's eating my disk" on macOS | General process forensics — use `ps`/`top` directly |
| Reclaiming space from OrbStack/Docker, caches, or local snapshots | A GUI hang/freeze occurred — see `macos-incident-postmortem` |
| `du` totals and free space disagree (purgeable snapshots, or CoW clones) | `launchservicesd` DB bloat specifically — see `launchservices-health` |
| Deleting a big tree freed far less than `du` promised | — |
## Platform Guard
This skill is **macOS-only**. APFS containers, `diskutil apfs`, `tmutil` local snapshots, and BSD `du` block-counting are Darwin-specific. Refuse to act if `uname -s` is not `Darwin`.
```bash
test "$(uname -s)" = "Darwin" || { echo "macos-plugin: not Darwin, refusing"; exit 1; }
```
This skill **cross-references, does not duplicate**, the Linux-focused `disk-full-recovery` rule (root partition, `~/.cache`, apt/journalctl). The APFS / Time Machine / OrbStack angle below is the macOS-specific complement.
## Core Expertise
The single most-important macOS fact: **APFS shares free space across every volume in a container.** A per-volume `Capacity %` is therefore misleading — the sealed system snapshot volume routinely reads **99%** while the container has tens of GB free. Trust the `Avail` column (or `diskutil apfs list`), never the `Capacity %`, or the investigation goes down the wrong path before it starts.
The second fact: **`du` lies about reclaimable space.** `tmutil` local snapshots hold purgeable space that `du` never counts, so `du` totals and reported free space can disagree by tens of GB. Check snapshots first when the numbers don't reconcile.
The third fact: **`du` also over-counts in the other direction — copy-on-write clones.** APFS reports a cloned file at *full* `st_blocks` even though its blocks are shared and cost nothing, so a `du` total can far exceed what deleting the tree would actually free. This is not fixable by switching tools: `dust`, `gdu`, and `ncdu` all read `st_blocks` and double-count identically. See [CoW clones](references/cow-clones.md).
## Step 1: Measure honestly (built-in CLI)
```bash
# Trust Avail, NOT Capacity % — APFS shares free space container-wide
df -h /
# The container's real free space, per-volume roles, and snapshot count
diskutil apfs list
# Top-level consumers, one level deep, staying on one filesystem (-x)
du -hx -d1 / 2>/dev/null | sort -h
du -hx -d1 ~ 2>/dev/null | sort -h
```
BSD `du` counts **allocated blocks**, so it reports the real on-disk size of sparse files (e.g. OrbStack's `data.img.raw` reads its true 33 GB, not its apparent size). Trust the number *for sparse files* — but not as a general rule: allocated blocks are also what makes `du` over-count clones (below), and they never include purgeable snapshot space.
When a size gates a decision ("how much will this free?"), or `df` barely moves after a delete, open [references/cow-clones.md](references/cow-clones.md) for the measured clone over-count, the per-package-manager table, and the `df`-delta recipe.
### Purgeable snapshot space (the thing `du` hides)
When `du` totals and `df` free space disagree, local Time Machine snapshots are the usual cause:
```bash
# List local snapshots holding purgeable space
tmutil listlocalsnapshots /
# Thin them (reclaims purgeable space invisible to du); needs sudo
sudo tmutil thinlocalsnapshots / 999999999999 4
```
## Step 1.5: Fast-path — the usual-suspects scan
Step 1 gives the honest *total*; this gives *where it went* without re-deriving the hunt by hand each time. `scripts/scan-suspects.sh` reads a public catalog of known reclaim targets (`scripts/suspects.tsv` — dev caches, build-artifact dirs, VM images), `du`s the ones that exist on this machine, and emits a ranked rollup with the **cleanup command and safety tier already attached**:
```bash
# Whole home (thorough; slower — du walks every tree)
bash "${CLAUDE_SKILL_DIR}/scripts/scan-suspects.sh" --home-dir "$HOME" --root "$HOME"
# Bound the build-artifact search to where projects live, raise the noise floor
bash "${CLAUDE_SKILL_DIR}/scripts/scan-suspects.sh" --root ~/repos --min-mb 1000
```
Output follows the structured `SUSPECT id=… tier=… size_kb=… cmd="…"` convention, plus a ranked human table and per-tier totals (`TIER_SAFE_KB=…`, `RECLAIMABLE_SAFE=…`). Work the tiers exactly as Step 4 — exhaust `safe` before `decision`, never blind-delete `userdata`.
⚠️ Those totals are `du` sums, so they are an **upper bound** wherever CoW clones apply, e.g. the `node_modules` / `.venv` suspects, which `uv` and `bun` clone from a global cache. Treat `RECLAIMABLE_*` as "at most this much"; confirm with a `df` delta. See [CoW clones](references/cow-clones.md).
**This augments Step 1, it does not replace it.** The scan only measures. The agent still reads `Avail` rather than `Capacity %`, checks snapshots when `du` and `df` disagree, and decides *which* `decision`/`userdata` items to remove.
**The `UNKNOWN` lines are the feedback loop.** Any big directory *not* in the catalog is surfaced as `UNKNOWN size_kb=… path=…`. When one recurs, add it to `suspects.tsv` **as a pattern** (never a machine-specific path — this repo is public) and open a PR; the catalog grows from real findings. Catalog columns and placeholders (`{dir}`, `{parent}`) are documented in the file's header. An `UNKNOWN` inside a git repo is usually a git-ignored build artifact; size and trace it with [references/repo-artifacts.md](references/repo-artifacts.md).
Two operational notes:
- **Runtime scales with disk size** — `du` walks every tree, so a full disk can take a minute or two. Bound with `--root <project-dir>` and `--min-mb`; or use `dust` (Step 3) for a parallel walk.
- **The catalog is the shared artifact; the scan *output* is machine-specific** — keep it in scratch, never commit it.
## Step 2: The OrbStack / Docker recovery play
OrbStack/Docker is frequently the dominant consumer (100+ GB seen). The VM image (`data.img.raw`) grows but **never shrinks on its own**. The recovery is: prune **inside** the VM, then OrbStack **auto-shrinks** the host image (unlike Docker Desktop, which needs manual reclaim).
```bash
# See what's reclaimable before pruning
docker system df
# Prune unused images + build cache (NOT volumes — see warning below)
docker system prune -a -f
```
A real session reclaimed **85 GB** this way (159 images / 57 GB reclaimable, 42 GB build cache), after which OrbStack auto-shrank the host image **103 GB → 33 GB**.
### Never blind-prune named volumes
`docker volume ls -f dangling=true` lists named volumes (`olmap_postgres_data`, `docker_neo4j_data`, …) that are "unused" **only because no container is currently running** — they hold live project databases. A blind `docker volume prune` would wipe them.
```bash
# Inspect first — named volumes are project data, anonymous-hash ones are scratch
docker volume ls -f dangling=true
```
Prune **only anonymous-hash volumes** (long hex names), never the human-named ones, and only after confirming with the user.
## Step 3: Third-party tooling
For a faster or interactive scan (`dust`, `dua`, `gdu`/`ncdu`, `diskonaut`) and their mise `aqua:` installs, open [references/third-party-tools.md](references/third-party-tools.md).
## Step 4: Tiered cleanup
Work top-down — exhaust the safe tier before touching anything that needs a decision.
| Tier | What | How | Notes |
|------|------|-----|-------|
| **Safe / regenerable** | Homebrew downloads | `brew cleanup -s` | Rebuilds on next install |
| | Go module cache | `go clean -modcache` | Re-downloads on next build |
| | pip cache | `pip cache purge` | |
| | uv cache | `uv cache clean` | **Blocks on the lock** while any `uvx`/`uv run` process (e.g. an MCP server) runs — read [troubleshooting](references/troubleshooting.md) before `--force` |
| | Playwright / Yarn / node-gyp caches | `rm -rf ~/Library/Caches/{ms-playwright,Yarn,node-gyp}` | Regenerable |
| **Needs a decision** | Docker images + build cache | `docker system prune -a -f` | OrbStack auto-shrinks host image after |
| | Local Time Machine snapshots | `sudo tmutil thinlocalsnapshots /` | Loses local restore points |
| **User data** | Named Docker volumes | confirm each | Live project databases — never blind-prune |
| | Anything in `~/Documents`, `~/Downloads` | user confirms | Irreplaceable |
## Quick Reference
One-line commands per step (suspect scan, honest free space, container truth, snapshots, Docker reclaim, machine-readable `dust` sizes): [references/command-reference.md](references/command-reference.md).
## Error Handling
When a number does not reconcile or a cleanup command fails, open the symptom → cause → fix table in [references/troubleshooting.md](references/troubleshooting.md).
## Related Skills
- `launchservices-health` — when `launchservicesd` DB bloat (not disk usage) is the concern
- `macos-incident-postmortem` — when a freeze/panic, not a full disk, is the symptom
- `disk-full-recovery` (user-global rule) — the Linux-focused complement to this APFS-specific skill
Auf GitHub ansehen