Clean up without it becoming a filesystem operation
cargo clean knows one project. cargo-broom finds the target directories across a whole directory tree, judges them by age and size, and first shows what a run would remove. Nothing happens without a confirmation.
--dry-run, --interactive, --yes$ cargo broom --dry-run --keep-days 0 --keep-size 50MB ~/GitHub/kestrel cargo-broom Summary (Dry Run) ℹ cargo-broom Reclaimable: 5.97 GiB ● Level A — Full Target Clean (Coarse) (1) - kestrel-core (5.97 GiB) at /Users/maxmustermann/GitHub/kestrel/target
--historycargo clean does not answer the question you actually have.
For one known project cargo clean is enough. Once dozens of checkouts sit on the disk the question changes: which of them take the space, which have gone cold, and what would a scheduled run remove tonight? cargo-broom adds recursive discovery, age and size policies, dry runs, interactive selection and machine-readable reports.
- 01Look first —
inspectand--dry-runtouch nothing - 02Then confirm — without
--dry-run,--interactiveor--yesthe run fails before the scan - 03Reversible —
--trashmoves targets to the OS trash instead of deleting them - 04Traceable —
--format jsonfor scripts, reports and scheduled dry runs
What the terminal prints.
$ cargo broom inspect ~/GitHub
cargo-broom Inspect — /Users/maxmustermann/GitHub
Found 3 Rust target directories occupying 7.89 GiB
• kestrel-core 5.97 GiB /Users/maxmustermann/GitHub/kestrel/target
• parser-core 1.19 GiB /Users/maxmustermann/GitHub/parser-core/target
• templates 750.55 MiB /Users/maxmustermann/GitHub/report-cli/crates/templates/target$ cargo broom --dry-run --keep-days 0 --keep-size 50MB ~/GitHub/kestrel
cargo-broom Summary (Dry Run)
ℹ cargo-broom
Reclaimable: 5.97 GiB
● Level A — Full Target Clean (Coarse) (1)
- kestrel-core (5.97 GiB) at /Users/maxmustermann/GitHub/kestrel/target
Next steps:
• Run `cargo broom -y` to reclaim 5.97 GiB
$ cargo broom -y$ cargo broom --dry-run --fine-only --clean-incremental --clean-doc ~/GitHub/kestrel
cargo-broom Summary (Dry Run)
ℹ cargo-broom
Reclaimable: 1.53 GiB
● Level B — Selective / Experimental Fine Clean (1)
- kestrel-core (pruned 0 stale fingerprints, 1 files) — 1.53 GiB at /Users/maxmustermann/GitHub/kestrel/target
Next steps:
• Run `cargo broom -y` to reclaim 1.53 GiB
$ cargo broom -yWhat each mode removes.
| Mode | Selection | What it removes |
|---|---|---|
| Default / Level A | Target is at least --keep-days old and, when supplied, at least --keep-size large | The complete standard local target/ |
| --coarse-only | Same policy as Level A, fine operations are rejected | The complete standard local target/ |
| --clean-incremental | Targets not selected for Level A | The caches under target/*/incremental |
| --clean-doc | Targets not selected for Level A | Generated target/doc output |
| --experimental-fine | Targets not selected for Level A | Fingerprints and matching hashed artifacts, selected by experimental age and duplicate heuristics |
| --toolchains <list> | Modifier for --experimental-fine | Also fingerprints built with a rustc other than the named toolchains |
| --installed | Modifier for --experimental-fine | The same, but keeps any currently rustup-installed toolchain |
| --fine-only | Disables Level A | Only the explicitly requested fine operations |
| --trash | Modifier for Level A, with any policy above | Moves the target to the OS trash instead of deleting it permanently |
Conflicting combinations are rejected by the CLI: --fine-only needs one of the fine operations, and --tests-only, --toolchains and --installed need --experimental-fine.
Where the tool deliberately stops.
Only standard targets go completely
Whole-target cleanup is limited to a standard local <workspace>/target. Shared targets, configured overrides, symlinks and targets outside the workspace are never removed wholesale.
No fallback to the coarse level
If a fingerprint cannot be parsed, cargo-broom skips fine cleanup for that project. It never falls back to deleting the complete target instead.
mtime, not atime
Staleness is judged by mtime. On noatime and relatime mounts atime is not updated on every read, which made artifacts look falsely recent. mtime is written by every actual Cargo build.
Running builds are skipped
A target currently locked by a Cargo process is skipped entirely at both levels, instead of racing the build for its files.
Confirmation is not configurable
By design: an unattended destructive run has to carry --yes in the command itself. The configuration file cannot shorten that.
Errors stay visible
Filesystem deletion errors are reported and produce a failing exit status. JSON reports go to stdout, process errors to stderr.
Four commands that delete nothing.
Inventory
Lists the discovered targets and their disk usage without applying any policy.
Diagnostics
Reports configured target overrides, unusually large targets and target lock findings.
Toolchains
Shows installed rustup toolchains not referenced by any rust-toolchain file below the directory. It uninstalls nothing.
Budget
Totals target disk usage across all discovered projects and, with --limit, flags when the budget is exceeded – along with the largest contributors.
Look first, then confirm.
- 1Install
From crates.io
The crate is published as
cargo-broomand is available ascargo broomonce installed. For the current main branch, install straight from the repository instead. It requires Rust 1.89 or newer.cargo install cargo-broom # or straight from the repository cargo install --git https://github.com/casoon/broom.git - 2Look
inspect and dry run
Neither command modifies files.
inspectshows what is there, the dry run shows what the current policy would select.cargo broom inspect ~/GitHub cargo broom --dry-run ~/GitHub - 3Clean
Confirmed run
Only
--yesor--interactivedeletes anything.--trashmakes the same run reversible, because the targets end up in the trash.cargo broom --yes --keep-days 14 --keep-size 50MB ~/GitHub
Configuration and a scheduled run.
root_path = "~/GitHub"
keep_days = 14
keep_size_mb = 50
# Unattended runs keep the experimental part switched off.
experimental_fine = false
fine_only = false
coarse_only = false
trash = false
history = false
hidden = false
ignore = ["archived-repo"]
skip = ["node_modules"]cargo broom --yes --keep-days 14 --keep-size 50MB \
--clean-incremental --clean-doc --history --color never ~/GitHubWhat cargo-broom is not meant for.
The repository names its limits itself – some of them are the reason for the conservative defaults.
Not for CI caches
A target/ persisted across CI runs grows for a different reason: every dependency bump leaves fingerprints from the previous Cargo.lock behind. That calls for cache-key-based invalidation, not an age heuristic. The README points to Swatinem/rust-cache, sccache and cargo-chef.
Fingerprint pruning is experimental
--experimental-fine is off by default and belongs in a reviewed dry run first – not in an unattended job.
Version 0.1.0
A safety-first alpha. Conservative target cleanup, inspection, JSON reporting and selective cache cleanup are available.
Active projects never pass the age gate
rust-analyzer runs cargo check whenever a project is open in an editor and keeps refreshing the target. Without --clean-incremental and --clean-doc such a project falls through to Skipped and reclaims nothing, however large its target grows.
Registry cleanup only sees below the directory
registry reads the Cargo.lock files below the given directory. Projects outside are not considered – hence the dry run over the broadest relevant root.
Recovery means rebuilding
Only generated Cargo output is removed. It is reproducible, but the next build takes correspondingly longer.
--toolchains and --installed is ported from cargo-sweep (MIT), which in turn mirrors Cargo’s own internal fingerprint hash.License text in the repository →Frequently asked questions.
Can a run delete something unrecoverable?
Only generated Cargo output is removed, which a build recreates. On top of that, --trash moves targets to the OS trash instead of deleting them permanently. What remains is a longer build after the cleanup.
What happens if I forget the confirmation?
A call without --dry-run, --interactive or --yes fails before the project scan even begins. The confirmation deliberately cannot be stored in the configuration file.
Why does a run reclaim nothing on my active project?
Level A only removes a target once nothing inside it has been touched for keep_days. rust-analyzer keeps refreshing the target while the editor is open, so the age gate never triggers. That is what --clean-incremental and --clean-doc are for: Cargo regenerates both cache directories on demand.
How often should a scheduled run fire?
The README suggests every two days rather than weekly, because incremental caches regrow within hours and the run itself is cheap. On macOS, StartCalendarInterval on odd days is more reliable than StartInterval, whose timer restarts on every reboot.
What exactly does --experimental-fine do?
It removes individual fingerprints and their matching hashed artifacts, selected by age and duplicate heuristics. Each entry is validated against its own fingerprint JSON rather than trusting the .fingerprint directory naming. If no JSON is parseable, the project is marked unsupported instead of guessing.
What does --history record?
One line per target per run with the resulting size, in a rolling 14-day log at ~/.local/state/cargo-broom/history.jsonl. Dry runs are never recorded. From the second entry on, the report gains a History metric – whether a target is growing back despite regular cleanup.
Start with the dry run.
Install it, run inspect over your own project tree and see how much is actually sitting there.