CleanDevMac — cdm — 100×40
  • macOS only
  • pure bash
  • MIT
  • zero telemetry

Your Mac is mostly
build cache now.

cdm is short for CleanDevMac — the name you type, and the name of the tool. It is a bash CLI that hunts down developer junk on macOS: DerivedData, node_modules, Go and Cargo caches, Electron blobs, dead app leftovers. It shows you every path and every byte before it touches anything.

Run it — nothing gets installed
curl -sSL https://github.com/cleandevmac/cdm/releases/latest/download/cdm | bash

Dry run — deletes nothing

curl -sSL https://github.com/cleandevmac/cdm/releases/latest/download/cdm | bash -s -- -n

It’s the recommended first run, and the flag most people never stop using.

Read it first — downloads, doesn’t run

curl -fsSL -O https://github.com/cleandevmac/cdm/releases/latest/download/cdm less cdm && bash cdm -n

Piping a stranger’s script into your shell is a fair thing to refuse. So don’t: cdm is one bash file, about 1,500 lines, with no dependencies — you can read the whole thing before you run it, and -n means the first run it does is a rehearsal. Read the source on GitHub ↗

Example scan 17.0 GB selected of 32.9 GB found
  • Developer caches & build artifacts 14.2 GB
  • Project junk, grouped per repo 9.4 GB
  • Docker / Podman 6.1 GB
  • Electron, browser & app caches 2.8 GB
  • Orphaned app data 386 MB

Illustrative scan — the sizes above are an example, not a promise. On a working developer’s Mac a dry run usually surfaces anywhere from a few hundred MB to many GB. Run -n to find out what yours holds. Categories are sorted biggest-first, exactly as the real TUI sorts them.

what cdm finds

Five groups. Each one is a set of rules, and each rule knows whether its target is regenerable — which is what earns it a permanent delete — or irreplaceable enough to go to the Trash instead.

Developer caches & build artifacts

Deleted
  • Xcode DerivedData
  • iOS DeviceSupport
  • Go build & module cache
  • npm
  • npx
  • pnpm
  • yarn
  • Turbo
  • Vite
  • webpack
  • Parcel
  • ESLint
  • Gradle
  • Maven
  • sbt / Ivy
  • Cargo
  • pip
  • uv
  • poetry
  • ruff
  • mypy
  • Ruby / Bundler
  • Bun
  • Deno
  • CocoaPods
  • SwiftPM
  • Composer
  • Bazel
  • Zig
  • kubectl
  • AWS
  • gcloud
  • Azure
  • Docker buildx
  • JetBrains
  • Playwright
  • Homebrew download cache

The safe, regenerable ones are pre-selected. The ones that cost real time or bandwidth to rebuild — the Maven repository, Playwright browsers — start unchecked. You opt in, not out.

Electron, browser & app caches

Deleted
  • Electron disk cache
  • GPU cache
  • code cache
  • VS Code
  • Claude
  • Slack
  • Chrome
  • Brave
  • Edge
  • Vivaldi
  • Arc
  • Firefox
  • Sentry
  • Crashlytics
  • Sparkle

Chromium and Firefox caches are handled per profile, so a work profile and a personal profile are separate rows. Crash-reporter and telemetry SDK caches are swept out with them.

Project junk, per repo

-p
  • node_modules
  • dist
  • build
  • target
  • __pycache__
  • git-ignored files

One git repo, one row. Not a pile of anonymous folder paths — so you clean the three projects you finished in 2023 and leave the one you’re mid-branch on. Off by default behind -p, because scanning $HOME is slow; an interactive run offers it once the cache scan is finished. Build directories are deleted; other git-ignored files go to the Trash.

Docker / Podman

Opt-in
  • stopped containers
  • unused images
  • build cache

Runs system prune -af, and only if you ask for it. Named volumes are never touched — your local Postgres survives.

Orphaned app data

To Trash
  • Application Support
  • Caches
  • Preferences

Leftovers whose owning app is gone. The installed-app list is read from LaunchServices, so a prefPane, a plugin or a driver is never mistaken for an orphan. Everything here goes to the Trash — if cdm guessed wrong, you put it back.

what cdm will never do

Nothing is deleted without an itemized confirmation.

Not a count. Not a category name. The actual paths, the actual sizes, and which of the three fates each one is headed for — printed on your screen, waiting on a y. A cleanup tool is a program you invite to destroy your own data. That deserves an argument, not a feature list.

Three things can happen to a file. You always know which.

Gone for good

Deleted permanently

Get it back?No — that’s the point. They regenerate.

  • Caches — npm, Go, Cargo, pip, Gradle, Homebrew downloads
  • Build output — DerivedData, dist, target, node_modules
  • Docker — stopped containers, unused images, build cache

These are deleted permanently, and that is the entire point: they regenerate. Putting 14 GB of DerivedData in the Trash frees nothing.

Recoverable

Moved to the Trash

Get it back?Yes. Open the Trash, put it back.

  • Orphaned app data — Application Support, Caches, Preferences
  • Git-ignored files — the ones that aren’t named build directories

Anything cdm had to infer was junk goes to the Trash instead of the void. Inference can be wrong. Restore is one ⌘Z away.

Protected

Never touched

Get it back?It never left. Protected regardless of any rule, including your own.

  • ~/Documents
  • ~/Desktop
  • ~/Downloads
  • ~/Pictures
  • ~/.ssh
  • iCloud Drive
  • App sandboxes, and Apple- or system-owned data
  • Docker named volumes — your local Postgres survives

No rule can reach these — not one you wrote, not one you pulled in with --patterns. The list is enforced below the rules, not by them.

  • Zero telemetry. cdm makes no network calls at all, except fetching its own rule JSON. There is no analytics, no phone-home, no opt-out to hunt for, because there is nothing to opt out of.
  • --dry-run shows what could be freed and deletes nothing. It’s the recommended first run, and the flag most people never stop using.
  • It is not clever. No heuristics guess at your intent. A path is cleaned because a rule you can open and read says so, or it is not cleaned at all.
  • Riskier items start unchecked. Maven repo, Playwright browsers, crash logs, project folders, orphaned data — all off until you say otherwise.
  • Every run is logged to ~/.cleandevmac/clean.log. What ran, what went, what it weighed.

keys, flags, and the log

The interactive run is the whole product: a list you drive with one hand. Categories sorted biggest-first, safe regenerable caches pre-selected, everything else waiting on you.

Keys

↑ ↓ · j k
Move the cursor
space
Toggle the category under the cursor
a · s · n
Select all · only the safe ones · none
enter
Show the exact paths and sizes behind a category
c
Clean — prints the itemized plan, confirm with y
q
Quit, having changed nothing

Flags

-n
Dry run — surfaces what could be freed, deletes nothing
--dry-run
The same thing, spelled out
-p
Include project junk — scans $HOME for git repos, so it takes a while
--patterns
Point cdm at your own rules — a directory or a URL
~/.cleandevmac/clean.log 2026-07-15 09:41:02 scan 5 categories · 32.9 GB found 2026-07-15 09:41:44 confirm 2 categories · 17.0 GB · plan accepted 2026-07-15 09:41:44 delete ~/Library/Developer/Xcode/DerivedData · 6.4 GB 2026-07-15 09:41:51 trash ~/Library/Application Support/Postman · 168 MB 2026-07-15 09:42:03 done 17.0 GB reclaimed · 0 errors

Every run, appended. If cdm removed it, there is a line saying so.

ls rules/

There is no hidden list. Every category cdm knows about is described by a JSON rule file. Add a target, drop one you don’t like, or fork the set entirely — no code changes.

  • index.jsonthe manifest
  • dev-caches.jsondev caches
  • app-caches.jsonapp caches
  • containers.jsondocker
  • project-junk.jsonrepos
  • orphans.jsonorphans
dev-caches.json — one rule, unedited{
  "icon":    "🔨",
  "name":    "Xcode derived data & device support",
  "method":  "rm",
  "default": true,
  "desc":    "Xcode build cache, device-support & documentation
             cache — rebuilt on next build.",
  "paths":   [
    "~/Library/Developer/Xcode/DerivedData",
    "~/Library/Developer/Xcode/iOS DeviceSupport",
    
  ]
}

method decides the fate — rm for things that regenerate, trash for things you might want back. default decides whether it arrives pre-selected, and desc is the why the tool prints next to it. Nothing in a rule file can reach the protected paths. Point cdm at your own set with --patterns <dir-or-url> and it will use yours instead.