Open source · MIT · v0.2.0

Your disk, explained.
Reclaimed the official way.

decruft is the Storage screen developers never got. One scan shows everything your machine has piled up: Xcode simulators, Go and Homebrew caches, Docker, the Android SDK, node_modules, Cargo targets, Flutter builds and forgotten agent worktrees. Then it removes only what you pick, through each tool's own command, and measures what it actually freed.

brew install matthew-eucaristo/tap/decruft

Then run decruft. Nervous? decruft --dry-run changes nothing. Other ways to install →

decruft — synthetic demo machine Try it: press s then c

A live replica of the TUI, with the numbers from the recorded demo machine. Nothing here touches your computer.

1Go binary
11developer categories
4family cleaners built in
0config, cache or state files
0network calls

The tour

Four screens. Every byte accounted for.

Overview

A full-width storage bar like Apple's, split into real developer categories. One row each: what removing it frees, how much is safe, how much needs a look, and your biggest safe wins.

enter open · space select · s all safe · c plan

List

Every item with its size, safety level and the exact command or action. A pane says what the item is, what will run, how it comes back, and why it got its level.

● safe ▲ review ■ protected ◉ live

Treemap

A squarified treemap in truecolor blocks, corrected for the shape of terminal cells. Zoom into a category, select tiles. Cleaned tiles stay put, hatched, so the map never jumps.

t treemap · enter zoom · esc out

Receipt

The bars spring to their new sizes and free space is measured from the disk, before and after. Trash, freed-now and delegated are kept apart, because they mean different things.

Under-delivering commands are flagged claimed, not freed

The real thing, recorded with VHS
Scan, browse Xcode, hand Flutter items back to flutterkill, zoom the treemap, clean every safe item, read the receipt. Recorded on a fake HOME built by demo/setup.sh, so nothing in it is from a real machine.

What it covers

One official command per area. Shown before it runs.

Built-in modules only offer what a tool can remove with its own documented command. No command means the item is shown with the owner's way to remove it, and never acted on. Every source is listed in docs/modules.md.

Xcode & Simulators

  • DerivedData per project, DeviceSupport per OSfolder → Trash (while Xcode is closed)● safe
  • Unavailable simulatorsxcrun simctl delete unavailable● safe
  • Simulator devices & runtimesxcrun simctl runtime delete <id>▲ review
  • Archives (hold your dSYMs)Xcode › Organizer■ protected

Go

  • Build cachego clean -cache● safe
  • Module cachego clean -modcache● safe
  • golangci-lint cachegolangci-lint cache clean● safe

Homebrew

  • Old versions, downloads, logsbrew cleanup --prune=all● safe
  • Unused dependenciesbrew autoremove▲ review

Package caches

  • CocoaPodspod cache clean --all● safe
  • uvuv cache prune● safe
  • pippip cache purge● safe

Containers

  • Stopped containers, dangling images, build cache. Never volumes.docker system prune --force▲ review

Android & Gradle

  • NDKs, system imagessdkmanager --uninstall <pkg>▲ review
  • Emulatorsavdmanager delete avd -n <name>▲ review
  • Gradle caches (it expires them itself)shown only■ protected

Node.js

via nodekill
  • node_modules, framework caches, package-manager caches, Node versionsnodekill apply --plan -↗ delegated

Flutter & Dart

via flutterkill
  • Build outputs, pub, Gradle, FVM and Android Studio cachesflutterkill apply --plan -↗ delegated

Rust & Cargo

via rustkill
  • Cargo target folders, registry and git caches, sccache, cargo install binaries, rustup toolchainsrustkill apply --plan -↗ delegated

Git worktrees

via fell
  • Forgotten git and AI-agent worktreesfell apply --plan -↗ delegated

System packages Linux

  • apt downloads, unused packages, journalsudo apt-get clean■ shown, never sudo
  • Unused Flatpak runtimes (user)flatpak uninstall --user --unused▲ review

Every byte is offered once. When two sources report the same folder, or one inside another, the built-in module wins, then the outer item. So fell's worktree hides nodekill's node_modules inside it, and a Tauri app's src-tauri/target is rustkill's, not nodekill's.

What macOS does not show you

“System Data” is not an answer.

Settings lumps developer data into “Developer” and “System Data”, and counts purgeable space as free. decruft opens the box.

  • Free space as the filesystem reports it (statfs). Every before/after number uses it.
  • Purgeable space: what macOS can free by itself, and what Finder quietly adds to “available”.
  • Inside System Data: simulator runtimes (often several GB each), swap files and APFS local snapshots. Shown, never touched.
  • The Trash, which still takes space until emptied. Without Full Disk Access, decruft says so instead of guessing.
  • What removal frees now: on APFS, blocks shared with clones and snapshots are left out, so a pnpm store or a cloned worktree is not over-promised.
macOS Settings
Developer System Data “Available”
decruft
Xcode Go Homebrew Docker Runtimes & snapshots Swap Trash Truly free

Illustration. Your own bar is drawn from your own disk.

Safety model

Built to be trusted with your disk.

Cleaners earn their reputation by what they refuse to do. decruft fails closed, everywhere.

⌘The official way only

Caches go through their own tool's command, folders without one go to the Trash, family items go back to their own tool. sudo commands and apps that manage their own data are shown, never run.

⎔Pinned to what you saw

Every item carries a fingerprint of its state: path, folder identity, level, command. Right before acting, decruft re-reads it, refuses on any change, and acts on the fresh copy only.

◉Live means hands off

DerivedData while Xcode runs, a booted simulator or its runtime, the SDK while Android Studio runs: shown as live, never selectable.

⌫Trash by default

Folders go to the Trash, where Put Back restores them. --permanent deletes instead. Official commands delete by design, so the plan and the receipt keep them apart.

⊘Fail closed

An unknown safety level, unreadable tool output or another contract version: the item is protected or the source is skipped, with a warning. Never a guess.

⛬Guarded folder moves

Absolute paths, inside your home folder, at least three levels deep, never through a symlink. Commands run by absolute path with fixed arguments, stdin closed, never through a shell.

◌Read-only modes

--list, --json and --dry-run never change anything. --yes takes safe items only, never review ones.

∅No residue, no network

decruft writes no config, cache or state and never contacts the network itself. Uninstall it and nothing is left behind.

the receipt
decruft · done in 2s
  ✓ Moved to Trash        608.2 MB    3 item(s)  freed when you empty the Trash; Put Back restores
  ✓ Freed now                1.8 GB    7 item(s)  xcrun simctl delete unavailable · go clean -cache · …
  ✓ Delegated                1.1 GB    7 item(s)  flutterkill ×2 · nodekill ×2 · rustkill ×3 (each tool's own rules)
    Free space  1.8 GB → 3.6 GB   +1.8 GB measured · about 608.2 MB more when you empty the Trash
    ↺ Build caches (Go, Homebrew, DerivedData) come back as you work; that is normal.

Three numbers that mean three different things, never summed into one flattering total. Free space is measured, not estimated.

The family

One umbrella. Specialists underneath.

decruft is the front door to four focused cleaners. brew install brings them along; decruft runs them in parallel through a versioned JSON contract and hands items back by {id, fingerprint} only, so each tool re-checks with its own rules. Each one also works on its own.

The contract is public: fell's contract. Unknown schema means the source is skipped with a warning. decruft never passes --force or --unlock.

Use

Interactive when you want it. Scriptable when you don't.

Plain table when output is not a terminal, JSON for your own tooling (home paths written as ~/…), headless cleaning of safe items for the brave. Fits 40 columns, and NO_COLOR switches to glyphs only.

# interactive
decruft
# interactive, but nothing changes
decruft --dry-run
# plain table, read-only
decruft --list
# machine-readable, read-only
decruft --json | jq '.items[0]'
# headless: clean every safe item (never review items)
decruft --yes
# roots for fell, rustkill, nodekill, flutterkill (default ~/dev)
decruft ~/work ~/side
# built-in modules only
decruft --no-family

Install

One command. The whole family.

brew install matthew-eucaristo/tap/decruft

Recommended on macOS and Linux. Homebrew installs fell, rustkill, nodekill and flutterkill as dependencies, and decruft runs them for their areas.

macOSfirst-class: Xcode, simulators, purgeable, snapshots, Trash
Linuxsupported: apt, journald and Flatpak join the cross-platform modules
Windowscross-platform modules: Go, packages, Docker, Android, family
Uninstalling is just as clean
brew uninstall decruft && brew autoremove -n   # lists what autoremove would take, including the family tools
brew autoremove                               # then run it if nothing on the list is something you use
brew untap matthew-eucaristo/tap            # if you no longer use the tap

decruft writes nothing to your disk, so there is nothing else to remove. With go install: rm "$(go env GOPATH)/bin/decruft".

Questions

Fair questions.

Will it delete something I need?

Only what you select, and only through the owning tool's documented command or a move to the Trash. Anything that needs judgement is marked ▲ review and is never taken by s or --yes. Archives, Gradle and anything behind sudo are shown with instructions and never acted on.

Why not just rm -rf ~/Library/Developer?

Because tools keep state about their own files. Go's module cache is read-only, so plain rm fails half-way; uv's cache goes inconsistent if you delete it by hand; simulator runtimes are registered with CoreSimulator. The official command is the one that leaves the tool in a correct state.

Why does it say “claimed, not freed”?

Because decruft measures free space before and after with statfs. If a command frees far less than it promised (APFS clones, snapshots holding blocks), the receipt says so instead of repeating the promise.

Does it phone home?

No. decruft makes no network requests and keeps no state. Commands it runs get a quiet environment (HOMEBREW_NO_AUTO_UPDATE, NO_COLOR, PIP_NO_INPUT…) so nothing updates itself either. This page loads nothing from third parties, too.

Do I need fell, rustkill, nodekill and flutterkill?

No. Homebrew installs them for you, but decruft skips any that are missing, and --no-family uses built-in modules only.

How is this different from other Mac cleaners?

It is a developer tool, not a system tuner: it knows what Xcode, Go, Homebrew and Docker are, uses their commands, explains every item, and proves the result with a measured receipt. Open source, MIT, a single binary you can audit.

I found a bug that touches the wrong thing.

That is a security issue. Please report it privately through GitHub Security Advisories, not a public issue.

Find out what is really on your disk.

brew install matthew-eucaristo/tap/decruft

Start with decruft --dry-run. It only looks.