---
title: "Disk Cleanup"
description: "Frees the disk that agents and worktrees leave behind: caches, merged worktrees, Docker build cache, old logs. Dry run lists safe, regenerable and ask-first items with sizes; --apply after you confirm. Use when the disk is nearly full. Never deletes branches."
canonical: "https://orchestkit.dev/docs/reference/skills/disk-cleanup"
---

# Disk Cleanup

Frees the disk that agents and worktrees leave behind: caches, merged worktrees, Docker build cache, old logs. Dry run lists safe, regenerable and ask-first items with sizes; --apply after you confirm. Use when the disk is nearly full. Never deletes branches.

<span className="badge badge-blue">Command</span> <span className="badge badge-green">low</span>

```bash title="Invoke"
/ork:disk-cleanup
```

<ContextualSkillSidebar slug="disk-cleanup" />

> **Disk Cleanup** Frees the disk that agents and worktrees leave behind: caches, merged worktrees, Docker build cache, old logs. Dry run lists safe, regenerable and ask-first items with sizes; --apply after you confirm. Use when the disk is nearly full. Never deletes branches.


# disk-cleanup, get the disk back from your agents

Running many agents and worktrees fills a disk fast: a fresh `node_modules` per
worktree, a build cache per tool, Docker layers, browser downloads, session logs.
This skill finds all of it, sorts it by how safe it is to remove, and removes only
what is provably safe, after you confirm.

## Critical rules

1. **Dry run first, always.** Run the script without `--apply`, show the inventory, and
   get an explicit yes from the user in the conversation before any `--apply`.
2. **`--apply` touches two tiers only.** SAFE (caches that refill on their own) and
   REGENERABLE (merged, clean, idle worktrees; rebuildable stores). ASK FIRST items are
   listed with the command to run. The script never runs them and neither do you without
   the user naming the item.
3. **Unreadable is KEEP.** A failed `git`, `gh`, `lsof` or `docker` call keeps the item and says why.
   Never remove on a guess.
4. **A worktree is removed only when all of these hold:** its tip is on the default branch
   (or equals the head of a merged PR), the tree is clean, it holds no ignored file outside
   a rebuildable folder (`node_modules`, `.venv`, `dist`, `build`, `.next`, `__pycache__` and
   the like; an ignored `.env.local` keeps the tree), it is not locked, no process has
   its working directory inside, nothing in it changed for 60 minutes, and it sits under
   the home directory. Removal is `git worktree remove` without force. The branch stays.
5. **Report the measured number.** The freed total is free space before versus after. The
   inventory total is an estimate and is labelled as one.

## Instructions

1. Dry run. From a repo you work in (the script scans its worktrees) or with `--root DIR`
   for a folder that holds several repos:

   ```bash
   python3 "${CLAUDE_SKILL_DIR}/scripts/disk_cleanup.py"
   python3 "${CLAUDE_SKILL_DIR}/scripts/disk_cleanup.py" --root ~/code
   ```

   Exit 0 means nothing to clean, 3 means findings, 2 means it could not look.
2. Show the user the inventory as printed. Lead with the verdict line, then name the three
   biggest rows and any KEEP lines that explain why a worktree was left alone.
3. Ask the user to confirm. Offer the narrower forms when they hesitate:
   `--tier safe` (caches only) or `--only npm-cache` (one item id, or a section such as `docker`).
4. Apply with the command the dry run printed under "To act on exactly this list". It
   pins `--only` to the ids the user just saw, so anything that became removable after
   the dry run (a worktree that merged or went idle meanwhile) is left alone:

   ```bash
   python3 "${CLAUDE_SKILL_DIR}/scripts/disk_cleanup.py" --apply --only npm-cache --only 'worktree:/path/to/tree'
   python3 "${CLAUDE_SKILL_DIR}/scripts/disk_cleanup.py" --apply --tier safe
   ```

   Expected: one line per action (done or failed), then `freed (measured, before and after)`.
5. For ASK FIRST rows, read the user the printed command and what it removes. Run it only
   when they confirm that specific row.
6. Optional: `--json` for a machine-readable inventory, `--report FILE` to save it. The report
   starts with a marker line and the script refuses to overwrite a file without it.

## What counts as what

| Tier | Meaning | Examples |
|---|---|---|
| 🟢 SAFE | Pure cache, the tool refills it | npm, pip, uv, Cargo, Go caches, Homebrew downloads, Xcode DerivedData, browser caches, Docker build cache and dangling images, agent debug logs older than 14 days |
| 🟡 REGENERABLE | Rebuilds, but costs time or bandwidth | merged worktrees, pnpm store, Playwright browsers, unused Docker images |
| 🔴 ASK FIRST | Might be someone's work | unmerged or detached worktrees, session transcripts, Downloads, Trash, Docker volumes, orphan agent processes |

Full path list per OS and the evidence behind each gate: `references/catalog.md`.

## Example output

```text
disk-cleanup dry run: 47.1 G reclaimable, 24.0 G more needs you

DISK  ~  (macOS)
[▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░]
 used 355.0 G    free 73.0 G    83 % full · 460.0 G total
────────────────────────────────────────────────────────────────────────
🟢 SAFE  caches that refill on their own
────────────────────────────────────────────────────────────────────────
🐳 Docker build cache                 ▓▓▓▓▓▓▓ 14.5 G  prune
🌐 Browser caches                     ▓▓▓▓░░░  8.9 G  clear
📦 npm cache                          ▓▓▓░░░░  6.2 G  clear
                                         ── 29.6 G
────────────────────────────────────────────────────────────────────────
🟡 REGENERABLE  merged worktrees and rebuildable stores
────────────────────────────────────────────────────────────────────────
🗂️ app/fix-login  merged PR #412      ▓▓▓▓░░░  3.9 G  remove
        KEEP  app/hotfix-7  dirty (3 files)
        KEEP  app/agent-3  process cwd inside (pid 4121)
🟢 29.6 G + 🟡 3.9 G = 33.5 G estimated  →  free 73.0 G → ~106.5 G
ℹ️  Sizes are allocated blocks, so sparse files read small.
```

## Examples

User says: "my disk is almost full, I run a lot of agents".
Actions: run the dry run, show the inventory, ask to confirm, run `--apply`.
Result: SAFE and REGENERABLE items removed, a measured freed total, ASK FIRST rows left for the user.

User says: "clean up old worktrees only".
Actions: dry run, then `--apply --only worktrees` after confirmation.
Result: merged clean worktrees removed, everything else untouched.

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| Every worktree reads KEEP "process list unreadable" | `lsof` missing or blocked (some sandboxes) | Run the script outside the sandbox, or install `lsof`. Never skip the check. |
| Squash-merged worktrees read as unmerged | `gh` missing or not logged in | `gh auth login`, then rerun. They stay ASK FIRST until then. |
| Docker items missing | Docker daemon not running | Start Docker and rerun. The caveat line says so. |
| Freed total is smaller than the estimate | Docker Desktop keeps its disk image at full size, APFS snapshots hold blocks | Trust the measured number. Compact Docker Desktop's disk image from its settings. |
| `git worktree remove` refused | The tree changed between the dry run and `--apply` | Rerun the dry run. It will classify the tree again. |

## Audit trail

Every applied action appends one JSON line to `~/.local/state/ork/disk-cleanup-ledger.jsonl`
(override with `--ledger`), plus one summary line per run.

## Scope

macOS first, Linux second. Windows is not supported. The skill frees disk space only:
it does not stop sessions, delete branches, or touch anything outside the home directory.


---

## References (1)

### Catalog

# disk-cleanup catalog and gates

Paths are relative to the home directory. A path that does not exist produces no row.
The list lives in `CATALOG` in `scripts/disk_cleanup.py`; add a row there and one test.

## Caches and stores

| id | tier | macOS | Linux | note |
|---|---|---|---|---|
| npm-cache | safe | `~/.npm/_cacache` | same | npm refills it |
| yarn-cache | safe | `~/Library/Caches/Yarn` | `~/.cache/yarn` | |
| pip-cache | safe | `~/Library/Caches/pip` | `~/.cache/pip` | |
| uv-cache | safe | `~/.cache/uv` | same | |
| cargo-registry | safe | `~/.cargo/registry/cache` | same | crates re-download |
| go-build | safe | `~/Library/Caches/go-build` | `~/.cache/go-build` | |
| homebrew | safe | `~/Library/Caches/Homebrew` | `~/.cache/Homebrew` | |
| xcode-derived-data | safe | `~/Library/Developer/Xcode/DerivedData` | none | next build is slower |
| browser-caches | safe | `~/Library/Caches/Google/Chrome`, `.../Chromium` | `~/.cache/google-chrome`, `~/.cache/chromium` | |
| agent-debug-logs | safe | `~/.claude/debug` | same | only files older than 14 days |
| agent-shell-snapshots | safe | `~/.claude/shell-snapshots` | same | only files older than 30 days (a long session still sources its own) |
| pnpm-store | regenerable | `~/Library/pnpm/store` | `~/.local/share/pnpm/store` | reinstall re-downloads |
| gradle-caches | regenerable | `~/.gradle/caches` | same | slow rebuild |
| playwright | regenerable | `~/Library/Caches/ms-playwright` | `~/.cache/ms-playwright` | browsers re-download |
| puppeteer | regenerable | `~/.cache/puppeteer` | same | |
| xcode-device-support | regenerable | `~/Library/Developer/Xcode/iOS DeviceSupport` | none | |
| agent-transcripts | ask | `~/.claude/projects` | same | session history, prints a `find ... -mtime +30 -delete` |
| codex-sessions | ask | `~/.codex/sessions` | same | same |
| downloads | ask | `~/Downloads` | same | prints a size listing, never a delete |
| trash | ask | `~/.Trash` | `~/.local/share/Trash` | |

Emptying a cache removes its contents and keeps the directory. A path that is a symlink is
reported as KEEP and never followed. A path that resolves outside the home directory, or
sits fewer than two levels below it, is refused.

## Docker

Read with `docker system df` and `docker images -f dangling=true`. Skipped quietly when the
`docker` binary is absent, with a caveat when the daemon is down.

| id | tier | action |
|---|---|---|
| docker-build-cache | safe | `docker builder prune -f` |
| docker-dangling-images | safe | `docker image prune -f` |
| docker-unused-images | ask | prints `docker image prune -a`, never runs it (a locally built image cannot be pulled back) |
| docker-volumes | ask | prints `docker volume prune`, never runs it |

## Worktrees

Found from the current repo or `--root DIR` (repos up to three levels deep). For each linked
worktree, in this order, the first match decides:

1. outside the home directory: KEEP
2. locked: KEEP
3. default branch unknown (`origin/HEAD`, then `origin/main`, `origin/master`): KEEP
4. process list unreadable: KEEP. A process whose working directory is inside: KEEP
5. any line in `git status --porcelain` (dirty or untracked): KEEP
6. changed within `--idle-minutes` (default 60): KEEP
7. tip is an ancestor of the default branch: REGENERABLE "merged"
8. `gh pr list --state merged` has the branch with a head sha equal to the local tip: REGENERABLE "merged PR"
9. otherwise ASK FIRST, "unmerged" or "detached", with the commit count ahead

Squash merges defeat git ancestry, which is why step 8 asks GitHub. Without `gh` those trees
stay ASK FIRST and a caveat says so. A tree whose directory is gone is a SAFE `git worktree prune`.

Why the gates exist: a merged-looking, clean tree can still hold unique work (about one in
four in an early measurement), an idle shell can hold a tree's directory, and a dirty tree in
a shared checkout can be another agent's live work. Classify first, delete only what the
content proves.

## Processes

Report only. A process with parent 1, owned by the current user, older than 24 hours, whose
command matches headless browsers, agent-browser, mcp, `http.server` or portless is listed
ASK FIRST with a `kill PID` line. The script never kills anything.

## Exit codes

0 clean, or every applied action succeeded. 2 could not look or refused (bad home, unsupported
platform, foreign report file). 3 findings in a dry run, or at least one action failed.
