DEVTRIM
measure · classify · trim
v0.8.2 Ratatui TUI Apache-2.0 Trash-first deletion Protected-path denylist --json
01 Philosophy
devtrim exists because cleaning a 900 GB Mac by hand is how good data dies. Three rules govern everything it does.
- Measure before cutting.
scanis read-only and always free to run. Sizes are estimated logical bytes; APFS clones, sparse files, Trash, and container VMs can differ. Incomplete traversal or metadata is an error, never a partial actionable estimate. - Classify by risk. Every finding carries a danger score from 1 to 10 that decides how hard you must confirm (see §06).
- Trim with a net. Filesystem deletions go to macOS Trash by default — recoverable from Finder. Permanent deletion requires an explicit
--shred.
The tool refuses to be clever about your data:
- Docker volumes are never pruned — a volume can hold a live production database.
- Xcode Archives are listed but exempt — they are release artifacts.
- Repos with recent commits are skipped, not swept.
02 Install & build
With Homebrew (installs the attested release binary plus completions and man page):
brew install mneves75/devtrim/devtrim
Or build from source:
git clone https://github.com/mneves75/devtrim
cd devtrim
cargo build --release --locked
cargo test --locked
Manual installs can generate shell integration directly:
devtrim completions zsh # bash | zsh | fish
devtrim manpage
No runtime dependencies beyond macOS itself. The current arm64 release build is about 1.5 MB and self-contained:
cp target/release/devtrim /usr/local/bin/
devtrim --version
03 Quick start
In an interactive terminal, start with the keyboard interface:
devtrim
Choose an operation, inspect its exact findings, and press a only when you intend to apply. Arrow keys or j/k navigate and scroll results; Esc cancels confirmation. Scanner warnings and partial-apply errors remain visible in the interface. Below 64×18, operation input is blocked until the terminal is resized. The TUI labels risk in text as well as color.
Everything starts as a dry-run preview. Look first:
devtrim scan
devtrim clean caches
Satisfied with the preview? Act — one decision at a time:
devtrim clean caches --apply -y
Reading the output:
37.5 GB danger:7 DerivedData /Users/you/Library/Developer/Xcode/DerivedData
└─ rebuilt on next build
- size — estimated logical bytes; actual immediately reclaimable space can differ.
- danger:N — confirmation weight; colors go green → amber → red.
- path — exactly what would be affected.
- └─ note — why removal is safe and how it comes back.
04 Command reference
Grouped by what you are here to do. Global flags are capability-scoped: read-only/report-only commands reject mutation flags they cannot honor instead of silently ignoring them. scan --shred intentionally changes the previewed action; Docker and simulator cleanup reject --shred because they execute exact typed commands rather than filesystem deletion actions; trash-empty rejects it because its preview is already permanent. The TUI owns its confirmation and rejects CLI mutation flags plus --json.
Inspect — nothing changes
tuiinteractive keyboard interface; also the bare-command defaultPREVIEW
Runs only when stdin and stdout are terminals. It calls the same scanners and apply owners as the CLI, shows textual risk labels, refuses CLI bypass flags, and requires a matching confirmation for the current preview. Bare devtrim prints help and exits nonzero instead when piped.
scanfull reclaimable-space reportLOW
Read-only report across every category: sizes, notes, danger levels. Add --json for machine output.
historyrecent journaled appliesLOW
Read-only view of the write-ahead apply journal: what was attempted, what completed, what errored. An attempt without a result is flagged as interrupted. --limit N bounds output; --json emits one {"operation":"history","entries":[…]} document.
largestbiggest directories under scan rootsLOW
Read-only ranking of the largest depth-≤2 directories under your scan roots (--top N, default 20). Unreadable entries are skipped and disclosed as a count — totals are lower bounds, never deletion authority.
icloudlarge files and locally allocated storageLOW
Recursively lists iCloud Drive files ≥ 100 MB with logical size and a local-allocation estimate. Sparse files, compression, clones, and dataless placeholders can make allocated bytes differ from logical bytes; devtrim does not claim to know upload progress.
Reclaim safely — regenerable state only
cachesHF models, npm/brew/uv cachesCAUTION
Moves regenerable caches to Trash. Hugging Face cleanup targets only ~/.cache/huggingface/hub; authentication tokens and other state in its parent remain untouched. Re-downloads automatically on next use — mind the bandwidth for large model caches.
node-modulesexact paths in conclusively stale reposCAUTION
Maps each node_modules to its owning Git repo, skips recent commits, and applies only the exact paths shown. Apply reasserts a real node_modules directory leaf and refuses symlinks plus .git, nested dependency-tree, or non-normal ancestors. Unknown Git activity is refused.
artifactscorroborated build artifacts in stale reposCAUTION
Deletes a directory only when its name is on a closed list and its ecosystem corroborates it: target beside Cargo.toml, .venv containing pyvenv.cfg, Pods beside Podfile, .next-family beside package.json, .gradle beside its build files, .build beside Package.swift, tool caches, or a directory carrying an exact CACHEDIR.TAG signature — always inside a conclusively stale Git repo. Ambiguous names (build, dist, vendor, bin, obj) are never matched. Corroboration, staleness, and build-process liveness are re-verified at apply.
xcodeDeviceSupport + DerivedDataCAUTION
Clears only direct DeviceSupport and DerivedData children; apply reasserts that exact category shape before removal. Both rebuild on next use. Archives exempt by design — they're release artifacts.
toolchainsold swift.org toolchainsCAUTION
Removes only direct .xctoolchain directories not referenced by any verified symlink in the Toolchains directory. A present but missing, broken, escaping, or invalid swift-latest reference blocks the scan; apply reasserts the exact direct-child shape. Xcode's built-in chain is unaffected. Reinstallable from swift.org.
dockerunused images + build cacheCAUTION
image prune -a + builder prune on the exact previewed local Unix-socket endpoint. Remote Docker contexts are refused. Volumes are never touched — a volume can hold your live database. The scan also reports the host-side VM disk image as a non-actionable finding measured in allocated blocks: docker system df measures inside the guest, while that sparse image is what the host actually pays for, and pruning does not shrink it. Restart the VM to trigger compaction. The disclosure appears even when the daemon is stopped, which is the one state where the cost is invisible to docker and still present.
uninstallpaths named for an app bundle idINFO
Resolves an app in /Applications or ~/Applications, reads its CFBundleIdentifier, and lists the support directories, caches, containers, preferences, saved state, HTTP storages, WebKit stores and launch agents named for that exact identifier. Matching is exact, never by display name: Amazon Kindle is com.amazon.Lassen, so a name match would find none of its files and would sweep everything containing the word. A conservative report, not an inventory: an app storing data under a product name is invisible to identifier matching — Visual Studio Code keeps ~/Library/Application Support/Code — and group containers are omitted entirely, because their names come from an arbitrary entitlement and a suffix rule would both miss real ones and misattribute others. Report-only: /Applications and most of ~/Library are refused by the protected-path boundary, and widening it would weaken every command rather than only this one.
optimizethree macOS maintenance tasksCAUTION
Three typed commands with fixed arguments: QuickLook thumbnail cache, user font caches, and the Launch Services database. --apply requires an explicit --task quicklook|fonts|launch-services, because one confirmation must not authorize unrelated work — plan_danger takes the maximum, so a cheap task would otherwise ride in on an expensive one's prompt. A task that cannot do what its name says is not offered: those needing root or hours of CPU, and DNS, because dscacheutil -flushcache does not clear the mDNSResponder resolver cache it would advertise. Preview by default; every run is journaled.
analyzeinteractive disk explorerINFO
Drill into disk usage largest-first with ↑/↓, Enter to open and Esc to go up. Measurement runs on a worker thread and streams in, so a directory that takes minutes to size never freezes the screen, and leaving a directory cancels its walk. Symlinks are reported at their own size rather than followed, a different device is never entered, and unreadable subtrees are disclosed as (partial) lower bounds. Never deletes — deletion in devtrim is always bound to a corroborated category, so use devtrim clean <category> for that.
statusmachine vitals and health scoreINFO
Uptime, load, memory, disk, battery, thermals, cumulative network, and the busiest processes, read through fixed-argv system tools whose parsers fail closed. A metric that cannot be read is reported as unavailable with its reason, never as a zero, and the health score names every input it was missing instead of scoring over the gap. Memory used is stated as active + wired + compressed, because counting macOS's reclaimable inactive pages reports a healthy machine at 96%.
installersstale downloaded installer archivesCAUTION
Direct children of Downloads and Desktop whose extension is on a closed list (dmg, pkg, mpkg, iso, xip) and untouched for longer than the active window. Scanning never recurses, so a bundled installer inside an extracted project tree is left alone, and formats that can carry source or user data are never matched. Apply refuses symlinks and any target outside those two directories. Not a cache — nothing re-downloads these automatically.
leftoversreport-only review hintsINFO
Lists possible agent scratch and .supergoal paths for manual review. It never deletes them because worktree or mission staleness cannot be proven from a name.
Nuclear — read twice, run once
simulatorsdelete exact previewed unavailable devicesCAUTION
Creates one finding per device whose runtime is gone, binds the exact UDID, and rechecks that device before deletion. --yolo may bypass confirmation, but it never adds erase all or any operation absent from preview.
trash-emptypurge ~/.Trash permanentlyNUKE
Preview with --confirm=<gb>; every eligible current top-level Trash item is shown as an exact target. Permanent purge additionally requires --apply, and the approximate size must match within ±2 GB. Items moved to Trash after preview are not added to the plan. A direct item named as an ASCII-case variant of .git is warned about and left in Trash instead of blocking the other exact items.
05 Safety model
Independent layers stand between a typo and a tragedy:
| Layer | Rule | Bypass |
|---|---|---|
| Preview default | without --apply, nothing mutates — ever | none needed (default) |
| Trash-first | filesystem deletes go to ~/.Trash | --shred |
| Typed target | only an exact validated path can reach removal; display text has no authority | nothing |
| Category authority | Xcode and toolchain apply reassert direct-child targets; node_modules apply reasserts its scanner's leaf and ancestor rules | nothing |
| Single sink | all filesystem removal passes one structurally enforced boundary | nothing |
| Identity verification | preview-time device/inode is re-verified through an open parent-directory handle; drift refuses | nothing |
| Directory preflight | foreign devices and nested Git repository/worktree markers are refused before Trash or permanent mutation | nothing |
| Command authority | typed capability must match the operation and validated Docker endpoint or simulator UDID | nothing |
| Risk notice | every human apply warns that cleanup can cause data loss | JSON remains machine-only |
| Danger gate | every interactive mutation confirms; critical plans require typed input (see §06) | -y skips y/N only; --yolo skips interactive prompts, never operation-specific acknowledgments |
| TUI approval | a typed approval must match the current preview and danger requirement | nothing; CLI bypass flags are rejected |
| Protected paths | hard denylist below | nothing |
| Protect config | user-listed protect paths are refused at the sink and filtered from previews | nothing |
| Liveness guard | repos owning a running build process, and DerivedData under a running xcodebuild, are refused; probe failure blocks | nothing |
| Apply journal | write-ahead attempt/result records surround every deletion and typed command; an unwritable journal blocks apply | nothing |
| Non-TTY guard | piped runs refuse mutation without explicit consent | explicit flag |
Protected paths
System roots (/System, /usr, /etc, /Applications, …) and user-critical dirs (~/.ssh, ~/.gnupg) are refused unconditionally. Under ~/Library, only these managed subpaths are eligible:
Developer/Toolchains · Developer/Xcode/iOS DeviceSupport
Developer/Xcode/DerivedData · Caches/Homebrew
Size escalation
| Total size | Minimum danger | Effect |
|---|---|---|
| > 1 GB | ≥ 5 | rebuildable-state warning |
| > 10 GB | ≥ 7 | elevated large-plan warning |
| > 50 GB | ≥ 8 | highest reversible warning |
06 Danger scoring
| Score | Meaning | Gate | Examples |
|---|---|---|---|
| 1–2 | read-only / trivially reversible | actionable CLI plans prompt unless -y; TUI requires y | scan, brew cache |
| 3–5 | regenerable caches and rebuildable state | prompt unless -y; non-TTY needs -y | caches, DeviceSupport, unavailable simulators, node-modules |
| 6–8 | larger or user-visible state loss | prompt unless -y | Docker, large sweeps |
| 9–10 | irreversible bulk deletion | typed numeric confirm | trash-empty |
$ devtrim trash-empty --confirm=14
dry-run no changes made; re-run with --apply and --confirm=14 to act
$ devtrim trash-empty --confirm=14 --apply
only the exact children from the displayed preview are eligible
07 Configuration
Optional file at ~/.config/devtrim.toml:
roots = ["~/dev", "~/sandbox"] # scan roots (default: ~/dev)
active_days = 30 # commit recency defining "active" repos
protect = ["~/dev/keep"] # never delete these paths or their children
CLI --root flags override config roots entirely. Unknown keys are rejected so a misspelled safety setting cannot appear active. protect entries expand ~, must be absolute, and are enforced deny-only at the deletion sink; a relative or malformed entry is an error.
08 Agent integration
Every reporting command accepts --json, emitting exactly one response envelope:
{
"operation": "xcode",
"applied": false,
"findings": [
{
"label": "DerivedData: Project-hash",
"path": "/Users/you/Library/Developer/Xcode/DerivedData/Project-hash",
"size_bytes": 40265318400,
"note": "build output; rebuilt on next build",
"danger": 7,
"action": { "type": "trash" }
}
],
"errors": []
}
- agents must pass
-yexplicitly to mutate non-interactively — silent approval is refused - actions are typed as
trash,shred,command,info, ornone; command argv is never parsed from display text "info"/"none"items must never trigger mutation logic
devtrim history --json emits its own single document — {"operation":"history","entries":[…],"errors":[…]} — where each entry carries numeric ts, phase, op, action, and either target or the exact argv. Agents can audit exactly what a previous apply attempted and what completed; an attempt without a result means the process was interrupted mid-apply.
AGENTS.md rule: “run devtrim scan --json before proposing disk changes; propose devtrim clean … rather than raw rm; audit with devtrim history --json.”09 Recipes
The quarterly deep-clean
devtrim scan— eyeball the landscape. No flags, no risk.devtrim clean caches --apply -y— regenerable caches go to Trash.devtrim clean node-modules— review stale repos first, then rerun with--apply -y.devtrim clean artifacts— corroborated build artifacts in the same stale repos; preview, then--apply -y.devtrim clean xcode --apply -y— DeviceSupport + DerivedData.df -h /System/Volumes/Data— verify. Trash still holds the safety net.
Inspecting large iCloud Drive files
devtrim icloud
Use this inventory to find large logical files and estimate their current local allocation. Manage downloads and eviction in Finder, which owns the user-visible iCloud state.
Pre-flight before a big clone or model download
devtrim scan --json | jq '[.findings[].size_bytes] | add // 0'
devtrim clean caches --apply -y
10 Troubleshooting
df says less space than expected after a clean
- Purgeable space accounting — macOS counts evictable content inconsistently between calls; trust trends over single readings.
- Docker sparse disk — space freed inside the VM returns only after TRIM compaction; restart the VM.
- Files went to Trash — Trash-first means space returns when Trash empties. That's the net working.
non-interactive run: re-run with -y…
You piped stdin without an explicit consent flag. Intentional. Add -y for routine ops; reserve --yolo for unattended maintenance windows.
the TUI requires an interactive terminal
Both stdin and stdout must be terminals. Use an explicit command such as devtrim scan --json in scripts, pipes, CI, or agent workflows.
skipping … because a build process is active
A running build or package process has its working directory inside that repo, or xcodebuild is running. Finish or stop the build and re-run. If the probe itself fails, devtrim blocks instead of guessing — that is the guard working.
refusing protected path
A root or computed target intersects the denylist. Check config roots; remember --root ~ makes nearly everything protected-by-design.
simulator storage differs from the preview
Each preview row measures and authorizes one unavailable device. devtrim rechecks that exact UDID before deletion; CoreSimulator may still compact bundles asynchronously afterwards.
an owner command failed
devtrim returns nonzero and includes the error in JSON. Failed actions are never counted as touched or reclaimed; fix the owner tool and preview again.