dev-clean-wsl
Reclaim disk space on WSL2 by purging dev caches and orphaned build artifacts, then guide the user through compacting the ext4.vhdx from Windows. Use when: (1) the user says 'dev-clean-wsl', 'clean ws
SKILL.mdskills/dev-clean-wsl/SKILL.md
GitHubdev-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 on a Windows drive —
C: by default, but it is often moved (e.g. to D:). The scan resolves the real
location from the registry (HKCU\...\Lxss → BasePath for $WSL_DISTRO_NAME). Two consequences drive this whole skill:
/looking healthy is not enough. The disk that actually fills up isusually the drive holding the vhdx, not the Linux
/. The scan reports both — read that drive first. The vhdx size minus data inside/is the compaction ceiling.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 the host drive only after the compaction step. Always finish with that step — skipping it makes the cleanup look like it did nothing from Windows' side. Often the vhdx has ballooned far past the real data (e.g. 674G file vs 204G used) — then compaction itself is the big win.
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, and the cwd for
pnpm store path (which is cwd-dependent: from /mnt/c it reports the Windows-side store). The deep home scan takes
~1 minute. It deletes nothing and prints: / vs host-drive usage, the vhdx path + size,
top home consumers, and per-category reclaimable sizes. Docker Desktop's vhdx
(AppData\Local\Docker\wsl) is not the distro's disk — never hand it out for compaction.
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 the host drive)
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="<vhdx Windows path from the scan>"
attach vdisk readonly
compact vdisk
detach vdisk
exit
Set expectations honestly: compaction reclaims only the free blocks inside the filesystem, so the vhdx shrinks to about the real data still inside WSL (the scan's "data inside /"). State the rough before/after free numbers for the host drive 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 — or, if it was moved
off C:, by reading BasePath under HKCU\Software\Microsoft\Windows\CurrentVersion\Lxss.
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.