dev-clean-wsl
Free space on a WSL2 machine, then hand the user the one step they must run themselves to make that space real.
The crux (why WSL is different)
On WSL2 the Linux filesystem is a single ext4.vhdx file that lives on the
Windows C: drive. Two consequences drive this whole skill:
/looking healthy is not enough. The disk that actually fills up isusually
C:, not the Linux/. The scan reports both — readC:first.Deleting files inside WSL does not shrink the vhdx. The vhdx only grows; freed
blocks stay allocated until the vhdx is compacted from Windows. So every purge below is "banked" inside WSL and becomes real free space on
C:only after the compaction step. Always finish with that step — skipping it makes the cleanup look like it did nothing from Windows' side.
wsl --shutdown (required for compaction) kills this session, so compaction is
always handed to the user as commands to run in a Windows terminal — never run
it from inside WSL.
Step 1 — Scan (read-only)
bash $HOME/.claude/skills/dev-clean-wsl/scripts/scan.sh [code-root]
code-root is optional (defaults to $HOME/repos, else $HOME) — the directory
searched for node_modules and orphaned target/ dirs. The deep home scan takes
~1 minute. It deletes nothing and prints: / vs C: usage, the vhdx path + size,
top home consumers, and per-category reclaimable sizes.
Step 2 — Present findings and confirm
Turn the scan into a short table grouped by safety, then get the user's pick (AskUserQuestion works well for multi-select). Do not delete large or ambiguous things silently — these caches are recoverable but rebuilds/re-downloads cost time.
Safety tier, highest-value-and-safest first:
| Tier | Item | Why safe |
|---|---|---|
| Free (zero cost) | Orphaned in-repo target/ when a global target-dir redirect is active |
Nothing builds there anymore — pure dead weight (often the single biggest win) |
| Free (zero cost) | Orphaned pnpm store/vN (older than the active version) |
pnpm store prune never touches old store versions; they are abandoned |
| Safe | cargo debug/incremental, release/incremental |
Pure incremental cache; only slightly slows the next build |
| Safe | cargo registry/src + registry/cache |
Re-fetched on demand (keep registry/index — refresh is slow) |
| Safe | pnpm store prune on the active store |
Drops only unreferenced packages |
| Safe | npm cache clean --force (~/.npm) |
Re-downloaded on demand |
| Costs a rebuild | active cargo target debug/deps (the big GB) |
Only cargo clean / rm reclaims it; forces a full cold rebuild |
| Reinstallable | idle projects' node_modules |
pnpm/npm install restores them; clear projects you are not actively on |
Leave alone unless the user explicitly names them:
User data, not cache — e.g.
~/youtube-dl,~/Downloads, media. Never auto-delete.Published/intentional build outputs —
dist/, packaged release tarballs.These are project-specific and may be wanted; do not treat a generic
dist/as cache.
Step 3 — Purge (only what the user confirmed)
Safety rules:
Relative-path deletes only.
cdinto the parent, thenrm -rf ./name—never
rm -rf /absolute/path. This is a guardrail against a mistyped absolute path.Check nothing is using a dir before clearing it. Before wiping a
target/,build dir, or CI-runner workspace, confirm no live build/process owns it (
ps -eo cmd | grep -iE 'cargo|Runner\.Worker'). Clearing a dir mid-build breaks it.Confirm "orphaned" before claiming zero-cost. An in-repo
target/is orphanedonly if a global
target-dirredirect (envCARGO_TARGET_DIRor~/.cargo/config.toml [build] target-dir) points elsewhere AND its newest artifact predates the switch. The scan prints both signals.
Per-category commands (adapt paths from the scan output):
# Orphaned in-repo target (zero rebuild cost — redirect is active elsewhere)
cd /path/to/repo && rm -rf ./target
# Orphaned old pnpm store version (active is vN; vN-1 etc. are dead)
cd "$(dirname "$(pnpm store path)")" && rm -rf ./v10 # example: active is v11
# cargo incremental cache (safe; minor next-build slowdown)
cd "$ACTIVE_TARGET/debug" && rm -rf ./incremental
# cargo registry (re-fetched; keep index)
cd "$HOME/.cargo/registry" && rm -rf ./src ./cache
# pnpm + npm caches
pnpm store prune
npm cache clean --force
# idle project's node_modules (reinstallable)
cd /path/to/idle-project && rm -rf ./node_modules
For the big active cargo debug/deps, only offer cargo clean / deleting the whole
target dir if the user accepts a full cold rebuild — call out the tradeoff explicitly.
After purging, re-run a quick df -h / and re-measure the touched dirs so the
reported total reflects reality.
Step 4 — Hand off the vhdx compaction (the step that frees C:)
This is the finale every run must end on. Pull the vhdx path from the scan output and give the user a ready-to-paste block for a Windows PowerShell terminal (not WSL):
wsl --shutdown
# Easiest (modern WSL — also keeps it sparse going forward):
wsl --manage <DistroName> --set-sparse true # DistroName from: wsl -l -v
# …or a reliable one-time compaction via diskpart:
diskpart
select vdisk file="C:\Users\<you>\AppData\Local\wsl\{GUID}\ext4.vhdx"
attach vdisk readonly
compact vdisk
detach vdisk
exit
Set expectations honestly: compaction reclaims only the free blocks inside the
filesystem (i.e. roughly what was just purged), so the vhdx — and C: usage — stays
about as large as the real data still inside WSL. State the rough before/after C:
free numbers from the scan so the user knows what to expect.
If the scan could not find the vhdx, have the user locate it on Windows with
Get-ChildItem -Recurse $env:LOCALAPPDATA -Filter ext4.vhdx.
Bonus — stale Codex processes (EMFILE / inotify exhaustion)
Not a disk problem, but the same "WSL slowly rots" family: orphaned codex plugin
brokers pile up (~3 processes each) and each eats inotify instances; at
fs.inotify.max_user_instances=128 Vite starts failing with EMFILE. Sweep them:
node $HOME/.claude/scripts/codex-sweep.js
(A SessionStart hook runs this automatically once per 6h; the manual run is for when EMFILE already struck. See /codex-sweep for details.)
Belt and braces — raise the ceiling so accumulation between sweeps can't bite:
echo 'fs.inotify.max_user_instances=1024' | sudo tee /etc/sysctl.d/60-inotify.conf
sudo sysctl --system
Notes
Self-hosted CI runners sometimes show up as large dirs. Their
_work/isephemeral CI workspace (safe to clear when idle); fully removing a runner means stopping + uninstalling its root-owned systemd service first (
sudo ./svc.sh stop && sudo ./svc.sh uninstall), which needs the user's sudo password — hand them those commands rather than guessing.This skill assumes WSL2. On a non-WSL host the scan still reports reclaimable
caches, but the vhdx/compaction step does not apply — the script flags this.