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.

The tool refuses to be clever about your data:

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
Rust ≥ 1.88 (edition 2024) to build. Runtime target: macOS 13+.

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
The two blocks above are separate on purpose. Pasting preview and mutation together defeats the point of previewing.

Reading the output:

   37.5 GB  danger:7  DerivedData  /Users/you/Library/Developer/Xcode/DerivedData
           └─ rebuilt on next build

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

Data-loss warning. devtrim is provided AS IS, without warranties. Cleanup can delete files; applying a plan means you accept the risk for the exact targets shown. Keep backups, review every preview, and grant Files & Folders, App Management, Automation, or Full Disk Access manually only when you understand the macOS request. See the Apache-2.0 license and Apple's Privacy & Security guide.

Independent layers stand between a typo and a tragedy:

LayerRuleBypass
Preview defaultwithout --apply, nothing mutates — evernone needed (default)
Trash-firstfilesystem deletes go to ~/.Trash--shred
Typed targetonly an exact validated path can reach removal; display text has no authoritynothing
Category authorityXcode and toolchain apply reassert direct-child targets; node_modules apply reasserts its scanner's leaf and ancestor rulesnothing
Single sinkall filesystem removal passes one structurally enforced boundarynothing
Identity verificationpreview-time device/inode is re-verified through an open parent-directory handle; drift refusesnothing
Directory preflightforeign devices and nested Git repository/worktree markers are refused before Trash or permanent mutationnothing
Command authoritytyped capability must match the operation and validated Docker endpoint or simulator UDIDnothing
Risk noticeevery human apply warns that cleanup can cause data lossJSON remains machine-only
Danger gateevery interactive mutation confirms; critical plans require typed input (see §06)-y skips y/N only; --yolo skips interactive prompts, never operation-specific acknowledgments
TUI approvala typed approval must match the current preview and danger requirementnothing; CLI bypass flags are rejected
Protected pathshard denylist belownothing
Protect configuser-listed protect paths are refused at the sink and filtered from previewsnothing
Liveness guardrepos owning a running build process, and DerivedData under a running xcodebuild, are refused; probe failure blocksnothing
Apply journalwrite-ahead attempt/result records surround every deletion and typed command; an unwritable journal blocks applynothing
Non-TTY guardpiped runs refuse mutation without explicit consentexplicit 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
npm and Homebrew may report configurable cache paths; devtrim accepts only their exact cache namespaces and revalidates them when applying. The denylist cannot be disabled by any flag. If you find yourself wanting to, the correct tool is a different tool.

Size escalation

Total sizeMinimum dangerEffect
> 1 GB≥ 5rebuildable-state warning
> 10 GB≥ 7elevated large-plan warning
> 50 GB≥ 8highest reversible warning

06 Danger scoring

ScoreMeaningGateExamples
1–2read-only / trivially reversibleactionable CLI plans prompt unless -y; TUI requires yscan, brew cache
3–5regenerable caches and rebuildable stateprompt unless -y; non-TTY needs -ycaches, DeviceSupport, unavailable simulators, node-modules
6–8larger or user-visible state lossprompt unless -yDocker, large sweeps
9–10irreversible bulk deletiontyped numeric confirmtrash-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": []
}

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.

Pair with an 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

  1. devtrim scan — eyeball the landscape. No flags, no risk.

  2. devtrim clean caches --apply -y — regenerable caches go to Trash.

  3. devtrim clean node-modules — review stale repos first, then rerun with --apply -y.

  4. devtrim clean artifacts — corroborated build artifacts in the same stale repos; preview, then --apply -y.

  5. devtrim clean xcode --apply -y — DeviceSupport + DerivedData.

  6. 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
Trash is your audit log. Everything cleaned today is restorable from Finder until you empty it.

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.