"Because your Mac deserves a spa day." ✨
cmm is a Go command-line tool that reclaims disk space on a developer's Mac without guessing. Every cleanup is a rule: it names exactly what it may touch, checks that nothing is using it, and is checked again right before anything is removed. You can look first (cmm plan), script it (cmm plan --json), and nothing is ever deleted without a person at a terminal.
- 🔎
cmm planshows what would go, how much space it frees, and which guards made it skip something. It never deletes and never writes anything. - 🧾
cmm plan --jsonprints the same plan as one JSON document (schema_version1) for scripts and apps. - 🧹
cmm applyshows the plan, waits for you to press Return, then cleans. Without a terminal it refuses. - 🖥️
cmm runis the interactive screen (Bubble Tea) on the same plan. It is a preview unless you pass--apply.
| Rule id | What | When it may go |
|---|---|---|
go-build |
Go build cache | no go process |
npm-cache |
npm download cache | no npm install/ci/... running |
cocoapods |
CocoaPods cache | no pod install/update running |
bun-cache |
bun cache | no bun install/add running |
uv-cache |
uv cache | uv not running and not using it |
xcode-module-cache |
Xcode module and compilation caches | Xcode and its build tools not running |
deriveddata-stale |
Xcode DerivedData folders |
untouched for 5+ days (or workspace gone for 3+ days), not in use, volume mounted |
lane-local-build |
stale Xcode build folders in ~/lane-builds |
same staleness rule |
lane-deps |
node_modules and ios/Pods of a lane in ~/lane-builds |
lane idle for 5+ days, not in use |
lane-build-output |
build output of a lane | lane idle for 5+ days |
jest-temp |
Jest temp cache | no jest process |
maestro-temp |
Maestro test output | older than 1 hour, no maestro or xcodebuild |
cursor-update |
Cursor's failed update (staged copy) | Cursor and its updater not running |
claude-versions, cursor-agent-versions |
old CLI versions | the newest, linked and in-use versions are kept |
simulators |
simulators whose runtime was removed | shut down, untouched for 3 days, not in use; removed with simctl delete |
homebrew |
old versions of installed formulae | brew cleanup, brew not running |
docker |
Docker's unused images, stopped containers and build cache (review) | only when you choose it; Docker is running and not building, pulling or composing; volumes are never offered |
Every item has a tier. Safe items are regenerable (caches, build output): a plain cmm apply takes them after Return. Review items are things you might want back (Docker's prunes): they are listed under "needs your choice", and are taken only when you say so on purpose: cmm apply --include-review, cmm apply --only docker, a selection, or by ticking them in cmm run or the app. Not even --yes takes them unasked.
Time Machine local snapshots are not part of the plan. cmm run offers them as a separate, manual, "harsh" option (see below).
- Allow-list, not deny-list. A path can be deleted only if a rule claims it and the policy accepts it: inside your home folder or per-user temp folder, and never a broad folder such as
~,~/Libraryor~/Library/Caches. - Checked again right before deleting. The rule's claim, the policy, symlinks and the guards are asked again for every item. If anything changed since the plan, the item is skipped with a reason.
- Never through a symlink. No symlink may sit between the root and the target, or be the target.
- Guards fail safe. If
lsoforpgrepcannot answer, the item is skipped, not deleted. - No human, no deletion.
cmm applyandcmm run --applyneed a terminal.applytakes a plain Return (anything else cancels);runtakes a typedy(nor Esc go back).apply --yesis the deliberate way to run unattended. Previews and--dry-runnever delete, even inapply --dry-run --yesorrun --apply --dry-run. - One failure never stops the rest. Every item is attempted; failures are listed and the exit status is non-zero.
- Honest sizes. Allocated bytes on the same filesystem, hard links counted once.
- Plans are metadata. A plan holds paths, sizes, tiers and reasons, never file contents.
A real apply appends one line per deleted (or failed) item to ~/.local/state/cmm/cleanup.log and, when something was cleaned, writes the time to ~/.local/state/cmm/last-run. plan, apply and run then say Last cleanup: 2026-10-14 (3 days ago). Plans, previews, dry runs and refusals write nothing. If the log cannot be written you get a warning; the cleanup still happens.
Two more private files in the same folder feed cmm history: history.jsonl (one JSON line per real apply that deleted or failed something: totals, free space before and after, the items) and free.jsonl (free-space samples, noted by cmm sample when one is due: 30 minutes since the last, or 256 MiB of movement). They hold paths and sizes only, are append-only, are never written through a link, and nothing that decides what may be deleted ever reads them.
To get started, you'll need Go installed on your system.
go install github.com/david-saint/cmm@latestIf you prefer to build it yourself or want to contribute:
-
Clone the repository:
git clone https://github.com/david-saint/cmm.git cd cmm -
Build the binary:
go build -o cmm .
cmm plan # look: nothing is deleted
cmm plan -v # list every item, not just the biggest per group
cmm plan --json # the plan for scripts and apps
cmm apply --dry-run # the plan, then stop
cmm apply # the plan, then press Return to clean
cmm apply --only go-build,npm-cache # limit to some rule ids
cmm apply --include-review # also take review-tier items (Docker's prunes), after Return
cmm apply --select-file sel.json # limit to the item paths in a JSON file (can only narrow the plan)
cmm run # interactive preview
cmm run --apply # interactive; type y to clean
cmm history # past cleanups and how free space has moved (reads only)
cmm history --json --days 30 # the same for programs: runs newest first, free-space timeline oldest first
cmm sample # note the free space for the history (no scan, well under a second)- Use the arrow keys to move and Space to select a group (or expand its items).
- Enter moves on: scan, results, and (with
--apply) the question. - Only a typed
yconfirms a deletion; Return never does. - The finished screen reports what was really freed, skipped and failed.
- Time Machine snapshots appear last, flagged Harsh.
cmm plan --json prints one document on stdout (schema_version 1: generated_at, host, items, groups, skipped, total_bytes). The format is described in the engine track's spec and pinned by a golden test.
cmm history --json prints one document (schema_version 1: generated_at, host, runs, samples, unreadable_lines); the timeline in samples holds the notes taken by cmm sample and both ends of every cleanup, UTC whole seconds. It is described in the history track's spec and pinned by a golden test.
An app that shows the plan and lets a person tick items cleans through the same guarded engine:
cmm apply --yes --json --select-file selection.json # selection.json: ["<path from the plan>", ...]cmm plans again and keeps only the selected paths that are in that fresh plan, so a selection can only narrow a plan, never widen it; any other path is reported as skipped. Every item is still checked again right before it is removed. --json prints one event per line on stdout (start, one item per path, done) and nothing else; it needs --yes (the caller has a person's confirmation for exactly this selection) or --dry-run. The events are described in the app track's spec.
cmm looks tools up on a PATH that includes /opt/homebrew/bin and /usr/local/bin, so a copy started from Finder still finds brew. A reader that goes away (an app quitting) does not stop a confirmed cleaning half way; SIGINT does.
Docker's prunes free space inside Docker's own disk image, which macOS may take a while to give back. Such items say so ("frees_inside"), and done.freed_bytes counts only what the disk itself got back (done.freed_inside_bytes holds the rest), so "freed" and "free space afterwards" are never inflated by space the disk does not show.
app/macos holds a small SwiftUI app (macOS 14 or later) on top of that interface: it shows the plan, lets you tick what you want gone and cleans exactly that through cmm. It never deletes anything itself.
cd app/macos && ./build-app.sh && open build/Cmm.appSee app/macos/README.md for what it does and does not do, its safety rules and its tests.
We love contributions! cmm follows a strict Test-Driven Development (TDD) workflow managed via the conductor methodology.
- Pick a task: Check the
conductor/tracks/for open items. - Red Phase: Write a failing test in
pkg/orcmd/. - Green Phase: Implement the minimum code to make it pass.
- Refactor: Clean up your code while keeping the tests green.
- Coverage: Ensure you hit at least 80% code coverage.
Read the full Workflow Guide for more details.
- Tests never touch real files. They build a sandbox (
pkg/sys/systest) with a fake home, a fake temp folder, a fake command runner and a fixed clock. While a test binary runs, the engine panics instead of deleting (or writing its log) outside a temporary folder. - Boundaries are tested. Only
pkg/sysstarts processes and onlypkg/enginedeletes files (the one listed exception is the manual Time Machine module);pkg/engine/boundaries_test.goreads the source and fails when that stops being true. - Never
go installwhile developing: it would overwrite thecmmyou use. Build into a scratch folder instead (go build -o /tmp/cmm-dev .).
Distributed under the MIT License. See LICENSE for more information.
Built with ❤️ and plenty of ☕️ by David Saint.