- 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.
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 ↗
- 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.
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.
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.
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
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.