- 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 2,000 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
- sccache
- Android Studio
- emulator snapshots
- unused Android system images
The safe, regenerable ones are pre-selected. The ones that cost real time or bandwidth to rebuild — the Maven repository, Playwright browsers, Android system images — start unchecked. You opt in, not out. An Android image is only listed when no AVD on your Mac boots from it; the ones your emulators use never show up.
Electron, browser & app caches
Deleted- Electron disk cache
- GPU cache
- code cache
- VS Code
- Claude
- webview partitions
- Slack
- Chrome
- Brave
- Edge
- Vivaldi
- Arc
- Firefox
- Sentry
- Crashlytics
- Sparkle
- app-updater payloads
- Claude Cowork VM image
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. Claude desktop’s ~10 GB Cowork VM image is offered too — unchecked, re-downloaded on next use, with your VM session data kept.
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. The one thing that reaches outward is
bon the prompt after a clean, which hands the donate link to your browser — nothing is ever sent. - --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. It opens with what your disk looks like — total, used, free — and closes with what it looks like now.
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
Then, once a clean has run
- r
- Scan again, and see what is left
- b
- Open the donate link in your browser — b as in buy me a coffee
- q
- Quit — the default, and what every other key does
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.
An optional procs list names the apps that rewrite those caches while running, so
cdm can tell you to quit them first. One special kind, android-images,
takes its globs from images and drops any system image an AVD still uses.
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.