
disk-hygiene
macOS disk cleanup, cache pruning, stale file detection, and Downloads triage. TRIGGERS - disk space, cleanup, disk usage
- Installs
- 268
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/terrylica/cc-skills --skill disk-hygiene- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads disk-hygiene automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
Disk Hygiene
Audit disk usage, clean developer caches, find forgotten large files, and triage Downloads on macOS.
Self-Evolving Skill: This skill improves through use. If instructions are wrong, parameters drifted, or a workaround was needed — fix this file immediately, don't defer. Only update for real, reproducible issues.
When to Use This Skill
Use this skill when:
- User asks about disk space, storage, or cleanup
- System is running low on free space
- User wants to find old/forgotten large files
- User wants to clean developer caches (brew, uv, pip, npm, cargo)
- User wants to triage their Downloads folder
- User asks about disk analysis tools (dust, dua, gdu, ncdu)
TodoWrite Task Templates
Template A - Full Disk Audit
1. Run disk overview (df -h /System/Volumes/Data && major directories)
2. Audit developer caches (uv, brew, pip, npm, cargo, rustup, Docker)
3. Scan in-repo build artifacts (Rust target/, .venv, node_modules — often the biggest, see Phase 2.5)
4. Scan for forgotten large files (>50MB, not accessed in 180+ days)
5. Present findings with AskUserQuestion for cleanup choices
6. Execute selected cleanups
7. Report space reclaimed
Template B - Cache Cleanup Only
1. Measure current cache sizes
2. Run safe cache cleanups (brew, uv, pip, npm)
3. Report space reclaimed
Template C - Downloads Triage
1. List Downloads contents with dates and sizes
2. Categorize into groups (media, dev artifacts, personal docs, misc)
3. Present AskUserQuestion multi-select for deletion/move
4. Execute selected actions
Template D - Forgotten File Hunt
1. Scan home directory for large files not accessed in 180+ days
2. Group by location and type (media, ISOs, dev artifacts, documents)
3. Present findings sorted by size
4. Offer cleanup options via AskUserQuestion
Phase 1 - Disk Overview
Get the lay of the land before diving into specifics.
/usr/bin/env bash << 'OVERVIEW_EOF'
echo "=== Disk Overview ==="
# MUST be /System/Volumes/Data, NOT `/`. On APFS (Catalina+) `/` is the SEALED
# READ-ONLY system volume and reports a fixed ~10GB used — it is not your disk.
# Verified 2026-07-31: `df -h /` said "10Gi used, 185Gi avail" on a machine that
# was actually 707GB used and 80% full. Reading `/` will make you conclude there
# is nothing to clean.
df -h /System/Volumes/Data
echo ""
echo "=== Major Directories ==="
du -sh ~/Library/Caches ~/Library/Logs ~/Library/Application\ Support \
~/.Trash ~/Downloads ~/Documents ~/Desktop ~/Movies ~/Music ~/Pictures \
2>/dev/null | sort -rh
echo ""
echo "=== Developer Tool Caches ==="
du -sh ~/.docker ~/.npm ~/.cargo ~/.rustup ~/.local ~/.cache \
~/.conda ~/.pyenv ~/.proto 2>/dev/null | sort -rh
OVERVIEW_EOF
Phase 2 - Cache Audit & Cleanup
Cache Size Reference
| Cache | Location | Typical Size | Clean Command |
|---|---|---|---|
| uv | ~/Library/Caches/uv/ or ~/.cache/uv/ | 5-15 GB | uv cache clean |
| Homebrew | ~/Library/Caches/Homebrew/ | 3-10 GB | brew cleanup --prune=all |
| pip | ~/Library/Caches/pip/ | 0.5-2 GB | pip cache purge |
| npm | ~/.npm/_cacache/ | 0.5-2 GB | npm cache clean --force |
| cargo | ~/.cargo/registry/cache/ | 1-5 GB | cargo cache -a (needs cargo-cache) |
| rustup | ~/.rustup/toolchains/ | 2-10 GB | rustup toolchain uninstall <name> (list with rustup toolchain list) |
| proto | ~/.proto/tools/<tool>/<version>/ | 0.2-2 GB each | proto uninstall <tool> <version> (list with proto versions <tool> --installed), or proto clean tools |
| Docker | Docker.app | 5-30 GB | docker system prune -a |
| Playwright | ~/Library/Caches/ms-playwright/ | 0.5-2 GB | npx playwright uninstall |
| sccache | ~/Library/Caches/Mozilla.sccache/ | 1-3 GB | rm -rf ~/Library/Caches/Mozilla.sccache |
| go-build | ~/Library/Caches/go-build/ | 5-25 GB | go clean -cache (or rm -rf if go not on PATH) |
| huggingface | ~/.cache/huggingface/ | 1-10 GB | rm -rf ~/.cache/huggingface/hub/<model> |
| Claude Code plugins | ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/ | 5-30 GB | Delete versions not in ~/.claude/plugins/installed_plugins.json, keeping the 3 newest (sort -V) for already-running sessions |
Safe Cleanup Commands (Always Re-downloadable)
/usr/bin/env bash << 'CACHE_CLEAN_EOF'
set -euo pipefail
echo "=== Measuring current cache sizes ==="
echo "uv: $(du -sh ~/Library/Caches/uv/ 2>/dev/null | cut -f1 || echo 'N/A')"
echo "Homebrew: $(du -sh ~/Library/Caches/Homebrew/ 2>/dev/null | cut -f1 || echo 'N/A')"
echo "pip: $(du -sh ~/Library/Caches/pip/ 2>/dev/null | cut -f1 || echo 'N/A')"
echo "npm: $(du -sh ~/.npm/_cacache/ 2>/dev/null | cut -f1 || echo 'N/A')"
echo ""
echo "=== Cleaning ==="
brew cleanup --prune=all 2>&1 | tail -3
uv cache prune 2>&1 # prune, NOT `clean --force` — see "uv cache lock" below
pip cache purge 2>&1
npm cache clean --force 2>&1
CACHE_CLEAN_EOF
Troubleshooting Cache Cleanup
| Issue | Cause | Solution |
|---|---|---|
uv cache lock held | Identify the holder first | See "uv cache lock" below — --force can be actively dangerous |
brew cleanup skips formulae | Linked but not latest | Safe to ignore, or brew reinstall <pkg> |
pip cache purge permission denied | System pip vs user pip | Use python -m pip cache purge |
| Docker not running | Docker Desktop not started | Start Docker.app first, or skip |
⚠️ uv cache lock — never reach for --force before naming the holder
uv cache clean waits 300 s for an exclusive lock, then errors. Earlier revisions of this skill said "use --force". That advice is wrong when the lock holder is a long-running service, and it is the same class of mistake as the api-gateway incident: mutating a cache underneath a live daemon.
Measured 2026-08-24: uv cache clean timed out, and the holders were
uv 1934 uv run python ~/code/search-svc/kernel/embed.py serve --model minishlab/potion-retrieval-32M
uv 2652 uv run python ~/code/search-svc/kernel/embed.py rerank --serve --model Xenova/ms-marco-MiniLM-L-6-v2
— both children of the launchd job com.search-svc.serve, up 1 h 37 m. These are persistent daemons, so the lock is never released and clean can never succeed on its own. Always identify the holder before deciding:
lsof ~/.cache/uv/.lock 2>/dev/null # exact PIDs holding the lock
ps -o pid,ppid,lstart,command -p <PIDS> # how long, and whose child
launchctl list | grep -i <service> # is the parent a launchd job?
Then pick by holder:
| Holder | Action |
|---|---|
Transient (uv sync you just ran) | Wait for it, or --force — genuinely safe |
| Long-running launchd/daemon | Do NOT --force. Either skip, or launchctl bootout → clean → bootstrap → verify |
| Unknown / can't identify | Skip. The cache is never worth an outage |
Prefer uv cache prune over uv cache clean in routine hygiene. prune removes only unused entries and leaves everything a live environment depends on, so it is the correct periodic-maintenance verb; clean nukes everything and forces a full re-download.
uv archive-v0 silently accumulates whole virtual environments
The uv cache's headline number is misleading, so classify before you judge it. archive-v0 is documented as unpacked wheel bodies that get hardlinked into each .venv — that part earns its keep. But it also accretes complete virtual environments (build envs / tool envs) that are never garbage-collected, and those are pure dead weight. Measured 2026-08-24 on a 69 GB uv cache:
| Entry shape | Count | Size |
|---|---|---|
Full venvs (contain pyvenv.cfg) | 210 | 39 GB |
| Genuine unpacked wheels | 1,577 | 10 GB |
So 78 % of archive-v0 was orphaned environments, not the dedup layer. Classify it — the split changes both the diagnosis and the remedy (prune, not clean):
du -sk ~/.cache/uv/archive-v0/* 2>/dev/null > /tmp/uv-all.txt
while read -r kb path; do
[ -f "$path/pyvenv.cfg" ] && echo "VENV $((kb/1024))MB $path"
done < /tmp/uv-all.txt | sort -k2 -rn | head
Also check hardlink counts before promising a number. uv hardlinks cache files into live .venvs, so deleting a cache entry with links>1 reclaims nothing:
stat -f 'links=%l size=%z %N' "$(find ~/.cache/uv/archive-v0 -type f -size +20M | head -1)"
# links=1 -> deleting truly reclaims; links>1 -> shared with a live venv, no gain
Don't let "Python is bloated" be the conclusion. In the same audit, Rust's per-repo target/ dirs totalled 57 GB against ~7 GB of Python .venvs — 8× more — because target/ is per-repo with no sharing while uv's archive is shared across every project. Report the measured split, not the folk wisdom.
Phase 2.5 - Project Build Artifacts (in-repo, regenerable)
The single most-missed category — check it on EVERY audit. Compiler and dependency output lives inside your repos, not under ~/Library, so the Phase 1/2 scans never see it. A single active Rust repo's target/ routinely hits 10-35 GB; across a dev tree these artifacts can dwarf every cache combined (one real audit: 62 GB of target/ + 20 GB of .venv). All of it regenerates on the next build — the only cost is recompile / re-sync time.
| Artifact | Dir name | Typical size | Regenerated by |
|---|---|---|---|
| Rust build | target/ | 1-35 GB each | cargo build |
| Python venv | .venv/ | 0.1-2 GB each | uv sync / uv venv |
| Node modules | node_modules/ | 0.1-0.5 GB each | npm / bun install |
| Zig cache | .zig-cache/, zig-cache/ | 0.1-1 GB each | next zig build |
Discover + size (point ROOTS at your code dirs)
/usr/bin/env bash << 'ARTIFACT_SCAN_EOF'
ROOTS=(~/code ~/projects ~/src)
for n in target .venv node_modules .zig-cache zig-cache; do
echo "=== $n (top 10 by size) ==="
find "${ROOTS[@]}" -maxdepth 5 -type d -name "$n" -prune 2>/dev/null \
-exec du -sh {} \; 2>/dev/null | sort -rh | head -10
done
ARTIFACT_SCAN_EOF
Safe deletion
Rust target/ — guard against false matches. Only delete a target/ that has a sibling Cargo.toml, so you never nuke an unrelated folder literally named "target":
/usr/bin/env bash << 'TARGET_CLEAN_EOF'
ROOTS=(~/code ~/projects)
find "${ROOTS[@]}" -maxdepth 5 -type d -name target -prune 2>/dev/null | while read -r t; do
[ -f "$(dirname "$t")/Cargo.toml" ] && rm -rf "$t" && echo "cleaned: $t"
done
TARGET_CLEAN_EOF
.venv / node_modules are safe to bulk-delete by name (regenerated on next uv sync / install):
find ~/code ~/projects -maxdepth 5 -type d -name .venv -prune -exec rm -rf {} +
⚠️ CHECK FOR DEPENDENT SERVICES FIRST — this is not optional
node_modules and .venv are only "safe to bulk-delete" for repos nobody is running. On 2026-07-31 a bulk delete took out api-gateway/node_modules; its launchd watchdog then failed 95 times and, in trying to restart the gateway, drove a Chrome launch that raised a macOS TCC prompt. The user reported it as a mysterious permission pop-up, and the disk cleanup was two steps removed from the symptom.
Build the exclusion list BEFORE deleting anything:
/usr/bin/env bash << 'DEPCHECK_EOF'
# Every repo backing a live launchd job — never delete artifacts inside these.
for p in "$HOME"/Library/LaunchAgents/*.plist; do
prog=$(plutil -extract ProgramArguments.0 raw "$p" 2>/dev/null) || continue
case "$prog" in "$HOME"/*) ;; *) continue ;; esac
d=$(dirname "$prog")
for _ in 1 2 3 4 5; do
{ [ -f "$d/package.json" ] || [ -f "$d/pyproject.toml" ]; } && break
d=$(dirname "$d"); [ "$d" = "$HOME" ] && break
done
[ "$d" = "$HOME" ] && continue # walked out; not a real repo match
echo "$d"
done | sort -u
DEPCHECK_EOF
Then SKIP any candidate path under one of those roots, and print the skip so the operator can see the guard fired. After cleanup, re-run the same list and assert each repo still has the manifest-matching directory (package.json → node_modules, pyproject.toml → .venv).
⚠️ Check EVERY manifest in the repo, not just the one at the root. The version above walks up from the launchd program to the first
package.jsonorpyproject.tomland stops — so for a repo whose service code lives in a subdirectory it verifies the wrong thing. Measured 2026-08-03 on~/code/search-svc: the root haspyproject.toml(so the check reported.venv=okandnode_modules=—, i.e. "not applicable") while the service actually needsts/node_modules, which was missing. The guard reported the repo healthy while its launchd job had been crash-looping 11,593 times. Enumerate instead:find "$repo" -name package.json -not -path '*/node_modules/*' -maxdepth 3 \ | while read -r m; do d=$(dirname "$m"); [ -d "$d/node_modules" ] || echo "MISSING $d/node_modules"; done find "$repo" -name pyproject.toml -not -path '*/.venv/*' -maxdepth 3 \ | while read -r m; do d=$(dirname "$m"); [ -d "$d/.venv" ] || echo "MISSING $d/.venv"; doneAlso note a Python venv can be present and still incomplete:
uv syncinstalls only the default dependency group.search-svcdeclared its embedding deps under[dependency-groups] embed, so the venv existed, importedpymupdffine, and failed onimport numpyuntiluv sync --group embedwas run. A directory existing is not the same as the dependencies being installed — where a repo documents a group/extra, restore it.
🔴 The walk-up finds NOTHING when the job execs a runner shim outside the repo. Both versions above start at
ProgramArguments.0and walk up the filesystem. But a launchd runner-shim policy (signed, distinctly-named shims in~/.local/libexec/or~/.claude/tools/launchd-runners/libexec/) puts the program in a directory that has no ancestor relationship to the repo at all — the walk-up terminates at$HOMEor, worse, lands on~/.claudeand reports that as the repo. The guard then emits a confident, entirely wrong exclusion list, and the real repo is deleted.Measured 2026-09-13: the exclusion list named
~/.claudefor 16 jobs and never mentioned~/code/terminal-scripts,~/code/trading-tools,~/code/private-monorepo— so the cleanup removed all three repos'.venv/node_modules, killingcom.example.terminal-snapshot(crash-safety snapshots),com.example.notify-telemetry(a Bun/TS daemon) andcom.example.keystroke-helper. Grep the shim for repo paths as well as walking up:for p in "$HOME"/Library/LaunchAgents/*.plist; do prog=$(plutil -extract ProgramArguments.0 raw "$p" 2>/dev/null) || continue [ -f "$prog" ] || continue # shims are often compiled binaries — `strings`, not `grep`, and search env-var # defaults too (e.g. SCRIPTS_REPO=/Users/.../code/terminal-scripts) strings "$prog" 2>/dev/null \ | grep -oE '/Users/[^/]+/(code|projects|src)/[A-Za-z0-9._-]+' | sort -u done | sort -uUnion that with the walk-up result. Also check the plist's
StandardOutPath/StandardErrorPathandWorkingDirectory— those frequently point into the real repo even whenProgramArgumentsdoes not.Also protect every repo a running process is using, not only launchd jobs. Live Claude sessions, dev servers and long-running daemons started by hand never appear in a plist. Union in the repos named by running command lines and process working directories:
{ ps -axo command=; lsof -d cwd -Fn 2>/dev/null | sed -n 's/^n//p'; } \ | grep -oE "$HOME/(code|projects|src)/[A-Za-z0-9._-]+" | sort -uMeasured 2026-10-15: the launchd scan named 6 repos; this added 5 more, which the launchd scan alone would have deleted from underneath live processes.
Corollary — a self-healing runner can be permanently poisoned while looking fine.
terminal-snapshot's shim rebuilds a missing venv, but caps attempts and persists the counter in~/.local/state/<job>/venv-bootstrap-attempts.txt. That counter had been sitting at5/5since Aug 21 and was never consulted, because the venv existed. Deleting the venv made the job hit the stale exhausted cap on its first try and refuse to self-heal:FATAL: venv bootstrap cap exhausted (5/5). Restoring deps is not enough — reset the attempt counter and kickstart, then confirm from the log that real work resumed (here,[auto-snapshot] wrote 17 tabs), not merelyexit 0.
Caveats:
- Run
pgrep -fl 'cargo build|rustc|zig build'first — never delete artifacts for a repo whose build/test is currently running. - It's a regenerable-cost tradeoff: the next build is a cold rebuild (minutes for big Rust crates). Worth it for idle repos; skip the one repo you're about to build.
cargo clean(run per-repo) is the tool-native equivalent ofrm -rf targetif you prefer.
Phase 3 - Forgotten File Detection
Find large files that have not been accessed in 180+ days.
/usr/bin/env bash << 'STALE_EOF'
echo "=== Large forgotten files (>50MB, untouched 180+ days) ==="
echo ""
# Scan home directory (excluding Library, node_modules, .git, hidden dirs)
find "$HOME" -maxdepth 4 \
-not -path '*/\.*' \
-not -path '*/Library/*' \
-not -path '*/node_modules/*' \
-not -path '*/.git/*' \
-type f -atime +180 -size +50M 2>/dev/null | \
while read -r f; do
mod_date=$(stat -f '%Sm' -t '%Y-%m-%d' "$f" 2>/dev/null)
size=$(du -sh "$f" 2>/dev/null | cut -f1)
echo "${mod_date} ${size} ${f}"
done | sort
echo ""
echo "=== Documents & Desktop (>10MB, untouched 180+ days) ==="
find "$HOME/Documents" "$HOME/Desktop" \
-type f -atime +180 -size +10M 2>/dev/null | \
while read -r f; do
mod_date=$(stat -f '%Sm' -t '%Y-%m-%d' "$f" 2>/dev/null)
size=$(du -sh "$f" 2>/dev/null | cut -f1)
echo "${mod_date} ${size} ${f}"
done | sort
STALE_EOF
Sudden loss: hunt by recent writes, not by age
When free space drops by tens or hundreds of GB within hours, the age-based scan above won't find it, and neither will du of the usual directories (it often isn't under $HOME). Look for what was written recently, across the whole Data volume, /private/tmp included:
find /System/Volumes/Data -xdev -type f -mmin -180 -size +2G -exec ls -la {} + 2>/dev/null | sort -k5 -rn | head
lsof "<file>" # still open? Deleting an open file frees nothing until the writer exits; truncate it instead
Measured 2026-10-16: the Mac went from 293 GB to 14 GB free (99%) in a few hours. The cause was one 262 GB file in /private/tmp, written by a cat run-*.log > run-all.log whose output matched its own glob, so cat kept reading back its own output until the disk filled. The last line was cat: stdout: No space left on device. No process had it open, so rm freed the space at once. Check the volume's snapshots first, too (diskutil apfs listSnapshots disk3s1): if a snapshot predates your deletions, it keeps their blocks allocated.
Two traps when hunting big files
1. Apparent size ≠ allocated size (sparse files). ls -l and find -size report the file's logical extent; du reports blocks actually on disk. A corrupted index or a database with a runaway seek produces a sparse file where these differ by orders of magnitude. Measured 2026-08-03 on a ChromaDB HNSW file:
ls -l link_lists.bin -> 2831.5 GB (apparent — impossible on a ~1 TB disk)
du -h link_lists.bin -> 174 GB (actual)
Always size candidates with du. If ls -l reports more than the disk holds, you have found a sparse file — and usually a bug worth reporting upstream, not just disk to reclaim. Never cp such a file (a naive copy expands the holes).
2. Applications quarantine their own wreckage — look for self-labelled dirs. Well-behaved data stores rename a damaged collection rather than deleting it, and the new name states the diagnosis. Grep the biggest directory for these markers:
find "$BIG_DIR" -maxdepth 2 -name '*corrupt*' -o -name '*.drift-*' \
-o -name '*.pre-rebuild-*' -o -name '*.bak-*' -o -name '*.quarantine*'
Before deleting one, prove it is unreferenced and superseded:
- no live process holds an fd inside it —
lsof -p <pid> | grep <dir>returns 0; - the app's own index/manifest does not mention its UUID;
- a healthy replacement exists and the app has completed a run since.
Real case: ~/.memory-store had grown to 190 GB, of which 175 GB was one directory named <uuid>.corrupt-20260802-160712.drift-20260802-160712 — the app had already diagnosed and set aside the damage from a 3-day crash loop, and a healthy 882 MB collection had replaced it. Deleting it took the volume from 82 % to 60 % full in one command.
Common Forgotten File Types
| Type | Typical Location | Example |
|---|---|---|
| Windows/Linux ISOs | Documents, Downloads | .iso files from VM setup |
| CapCut/iMovie exports | Movies/ | Large .mp4 renders |
| Phone video transfers | Pictures/, DCIM/ | .MOV files from iPhone |
| Old Zoom recordings | Documents/ | .aac, .mp4 from meetings |
| Orphaned downloads | Documents/ | CFNetworkDownload_*.mp4 |
| Screen recordings | Documents/, Desktop/ | Capto/QuickTime .mov |
Phase 4 - Downloads Triage
Use AskUserQuestion with multi-select to let the user choose what to clean.
Workflow
- List all files in
~/Downloadswith dates and sizes - Categorize into logical groups
- Present AskUserQuestion with categories as multi-select options
- Offer personal/sensitive PDFs separately (keep, move to Documents, or delete)
- Execute selected actions
Categorization Pattern
/usr/bin/env bash << 'DL_LIST_EOF'
echo "=== Downloads by date and size ==="
find "$HOME/Downloads" -maxdepth 1 \( -type f -o -type d \) ! -path "$HOME/Downloads" | \
while read -r f; do
mod_date=$(stat -f '%Sm' -t '%Y-%m-%d' "$f" 2>/dev/null)
size=$(du -sh "$f" 2>/dev/null | cut -f1)
echo "${mod_date} ${size} $(basename "$f")"
done | sort
DL_LIST_EOF
AskUserQuestion Template
When presenting Downloads cleanup options, use this pattern:
- Question 1 (multiSelect: true) - "Which items in ~/Downloads do you want to delete?"
- Group by type: movie files (with total size), old PDFs/docs, dev artifacts, app exports
- Question 2 (multiSelect: false) - "What about personal/sensitive PDFs?"
- Options: Keep all, Move to Documents, Delete (already have copies)
- Question 3 (multiSelect: false) - "Ongoing cleanup tool preference?"
- Options: dust + dua-cli, Hazel automation, custom launchd script
Disk Analysis Tools Reference
Comparison (Benchmarked on ~632GB home directory, Apple Silicon)
| Tool | Wall Time | CPU Usage | Interactive Delete | Install |
|---|---|---|---|---|
| dust | 20.4s | 637% (parallel) | No (view only) | brew install dust |
| gdu-go | 28.8s | 845% (very parallel) | Yes (TUI) | brew install gdu |
| dua-cli | 37.1s | 237% (moderate) | Yes (staged safe delete) | brew install dua-cli |
| ncdu | 96.6s | 43% (single-thread) | Yes (TUI) | brew install ncdu |
Recommended Combo
dustfor quick "where is my space going?" - fastest scanner, tree outputdua iorgdu-gofor interactive exploration with deletion
Quick Usage
# dust - instant tree overview
dust -d 2 ~ # depth 2
dust -r ~/Library # reverse sort (smallest first)
# dua - interactive TUI with safe deletion
dua i ~ # navigate, mark, delete with confirmation
# gdu-go - ncdu-like TUI, fast on SSDs
gdu-go ~ # full TUI with delete support
gdu-go -n ~ # non-interactive (for scripting/benchmarks)
Install All Tools
brew install dust dua-cli gdu
Note: gdu installs as gdu-go to avoid conflict with coreutils.
Quick Wins Summary
Ordered by typical space reclaimed (highest first):
| Action | Typical Savings | Risk | Command |
|---|---|---|---|
Rust target/ dirs (Phase 2.5) | 10-60 GB+ | None (cold rebuild on next cargo build) | find ROOTS -type d -name target + Cargo.toml-sibling guard |
Python .venv dirs (Phase 2.5) | Often ~0 GB | None (re-sync via uv sync) | uv hardlinks venv files from its cache: measure with df before/after, not du |
go clean -cache | 5-25 GB | None (re-downloads) | go clean -cache |
uv cache prune | 5-40 GB | None (drops only unused entries) | uv cache prune (check lsof ~/.cache/uv/.lock first) |
brew cleanup --prune=all | 3-10 GB | None (re-downloads) | brew cleanup --prune=all |
| Delete movie files in Downloads | 2-10 GB | Check first | Manual after AskUserQuestion |
| Prune old rustup toolchains | 2-5 GB | Keep current | rustup toolchain list then rustup toolchain uninstall <name> |
| Prune stale proto toolchains | 0.5-3 GB | Cross-check .prototools pins first | proto versions <tool> --installed, then proto uninstall <tool> <version> |
npm cache clean --force | 0.5-2 GB | None (re-downloads) | npm cache clean --force |
pip cache purge | 0.5-2 GB | None (re-downloads) | pip cache purge |
| Docker system prune | 5-30 GB | Removes stopped containers | docker system prune -a |
| Empty Trash | Variable | Irreversible | find ~/.Trash -mindepth 1 -delete (NOT rm -rf ~/.Trash/*) |
Post-Change Checklist
After modifying this skill:
- Cache commands tested on macOS (Apple Silicon)
- Benchmark data still current (re-run if tools updated)
- AskUserQuestion patterns match current tool API
- All bash blocks use
/usr/bin/env bash << 'EOF'wrapper - No hardcoded user paths (use
$HOME) - Append changes to evolution-log.md
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
uv cache clean hangs / times out after 300 s | Lock held by a uv process — often a persistent launchd daemon, so it never releases | lsof ~/.cache/uv/.lock to name the holder, THEN choose. Never blind --force. Prefer uv cache prune. See "uv cache lock" in Phase 2 |
rm -rf ~/.Trash/* aborts with no matches found and exits 1 | The shell is zsh, whose default nomatch makes an unmatched glob a fatal error — so rm never runs, and the non-zero status can abort a set -e script or be misread as a failed delete | Use find ~/.Trash -mindepth 1 -delete, which is glob-free, handles spaces/brackets in the movie-release filenames, and is a no-op on an empty Trash |
brew cleanup frees 0 bytes | Already clean or formulae linked | Run brew cleanup --prune=all |
find reports permission denied | System Integrity Protection | Add 2>/dev/null to suppress |
gdu command not found | Installed as gdu-go | Use gdu-go (coreutils conflict) |
dust shows different size than df | Counting method differs | Normal - df includes filesystem overhead |
| Stale file scan is slow | Deep directory tree | Limit -maxdepth or exclude more paths |
| Docker not accessible | Desktop app not running | Start Docker.app or skip Docker cleanup |
parse error near TASK_ID=$(pueue add ...) from heredoc with spaced paths | A user shell hook (e.g. pueue submission) re-parses the command string and breaks on ${var}/Path With Spaces/* globs inside heredocs | Write multi-line scripts to /tmp/<name>.sh first via Write tool, then invoke as bash /tmp/<name>.sh — bypasses the inline heredoc → hook re-quote path entirely |
| Freeing a remote Linux host's "old OS" partition | After an OS migration, the previous root left on the data drive looks like dead weight (usr, opt, boot, swap.img) | Before deleting it, read the host's migration docs and firmware boot entries (efibootmgr -v): it may be the documented rollback path for a soak period. No open files and no references does not mean unused. Measured 2026-10-15: a cleanup deleted a fallback root 25 days before its soak ended |
rsync -a src/ dest/ loosens a directory you created as 0700 | -a copies the source directory's own mode onto dest | When staging sensitive data, chmod 0700 after the rsync (or use --chmod=D0700,F0600) and check with stat before you rely on it |
| Disk fills overnight on a host where agents create git worktrees | Every worktree checks out its own copy of LFS-tracked data | Count worktrees and size each one's data dir; remove only clean, idle, fully pushed worktrees with git worktree remove. The lasting fix is in the repo: set lfs.fetchexclude for the bulk data and symlink it into worktrees |
| Removing a proto toolchain triggers immediate auto-reinstall | A project's .prototools pins the version you just removed; proto reinstalls it on the next invocation from that project when auto-install is on, and fails the invocation when it is off | Before proto uninstall <tool> <version>, grep all reachable .prototools files for the version. If pinned, leave it alone or update the pin first. Same applies to rustup toolchains vs. rust-toolchain.toml files in projects. |
Hook-safe multi-line scripts
If the user's shell environment has bash hooks that intercept tool calls (pueue, asciinema, etc.) and the heredoc pattern fails with cryptic parse errors, write the script to a temp file and invoke it:
# Instead of: bash << 'EOF' ... EOF
# Use: Write tool → /tmp/<task>.sh, then:
bash /tmp/<task>.sh
Single-line bash invocations like du -sh "$HOME/Library/Application Support"/Google/* 2>/dev/null | sort -rh | head work fine even with hooks installed — only multi-line heredocs containing spaced-path globs are problematic.
Post-Execution Reflection
After this skill completes, reflect before closing the task:
- Locate yourself. — Find this SKILL.md's canonical path before editing.
- What failed? — Fix the instruction that caused it.
- What worked better than expected? — Promote to recommended practice.
- What drifted? — Fix any script, reference, or dependency that no longer matches reality.
- Log it. — Evolution-log entry with trigger, fix, and evidence.
Do NOT defer. The next invocation inherits whatever you leave behind.
FAQ
What does the disk-hygiene skill do?
macOS disk cleanup, cache pruning, stale file detection, and Downloads triage. TRIGGERS - disk space, cleanup, disk usage
How do I install disk-hygiene?
Run npx skills add https://github.com/terrylica/cc-skills --skill disk-hygiene in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/disk-hygiene/ (personal) or .claude/skills/disk-hygiene/ (project).
Which AI agents can use disk-hygiene?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is disk-hygiene safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains disk-hygiene?
It is published by terrylica in terrylica/cc-skills. It has 268 recorded installs.
More from terrylica/cc-skills
mise-tasks
terrylica/cc-skills
Orchestrate multi-step project workflows using mise task definitions with dependency management and argument handling. Use whenever the user.
pandoc-pdf-generation
terrylica/cc-skills
PDF generation from markdown via Pandoc/XeLaTeX. TRIGGERS - markdown for PDF, print document, pandoc
mql5-indicator-patterns
terrylica/cc-skills
MQL5 indicator development patterns. TRIGGERS - MQL5 indicator, OnCalculate, indicator buffers, MetaTrader 5.
python-logging-best-practices
terrylica/cc-skills
Python logging with loguru, structlog, and orjson. TRIGGERS - loguru, structlog, structured logging
gdrive-access
terrylica/cc-skills
Access Google Drive via CLI with 1Password OAuth: list files, download, and sync folders. TRIGGERS - google drive, gdrive, drive folder, download drive, sync drive.
chezmoi-workflows
terrylica/cc-skills
Dotfile backup and sync with chezmoi. TRIGGERS - chezmoi, dotfiles, sync dotfiles, backup configs, cross-machine sync.
Popular other skills
hyperframes-cli
heygen-com/hyperframes
The HyperFrames CLI by job - scaffold, check, look at frames, edit the timeline, preview, render locally or in the cloud, publish, project history - plus reading its JSON and recovering from build or render failures.
triage
mattpocock/skills
Move issues and external PRs through a state machine of triage roles, categorise, verify, grill if needed, and write agent-ready briefs.
ask-matt
mattpocock/skills
Ask which skill or flow fits your situation. A router over the skills in this repo.
to-questionnaire
mattpocock/skills
Turn a decision you can't fully answer into a questionnaire for someone else to fill in.
loop-me
mattpocock/skills
Grill me about specs for the workflows I want to build, within this workspace.