Skip to content

Repository files navigation

voom — clean up the pink snow Then the Voom…
It went VOOM!
And, oh boy!
What a VOOM!

Now, don't ask me what Voom is.
I never will know.
But, boy!
Let me tell you it does clean up snow!

Dr. Seuss, The Cat in the Hat Comes Back

Reclaim every build artifact on your disk in one parallel pass — and prove each one before deleting it.

voom finds and removes build output anywhere in a tree: target/, dist/, __pycache__/, .gradle/, .zig-cache/ and the rest. A directory counts as an artifact only when a marker file proves its ecosystem, so a hand-written bin/ or vendor/ is never taken.

Every major ecosystem · marker-anchored classification · six deletion rails · a dry run that is the real pipeline · hierarchical TOML · a named tool-cache catalog · watch mode · git hooks

crates.io npm PyPI CI license: MIT Sponsor

Install · Usage · Safety · Caches · Suggest · Ecosystems · Configuration · Git hooks · Reference


voom previewing a tree with --dry-run, running the same sweep for real with identical results, and leaving a target directory that no Cargo.toml proved

A --dry-run, then the same sweep for real: identical to the byte, because the dry run is the same pipeline with the last step withheld. A symlinked target/ is refused rather than followed — and the target/ that no Cargo.toml proved is still there afterwards.


Installation

Channel Command
Homebrew (macOS/Linux) brew install goldziher/tap/voom
Cargo (Rust) cargo install voom
npm (Node.js) npm install -g @goldziher/voom
pip (Python) pip install voom-cli

The binary is called voom however you install it. The package names differ because voom was already taken on npm and PyPI — see ADR 0010.

Prefer prebuilt binaries? cargo binstall voom downloads a release archive instead of compiling from source.

Run without installing:

npx -y @goldziher/voom@latest --dry-run ~
uvx --from voom-cli voom --dry-run ~
Build from source
git clone https://github.com/Goldziher/voom.git
cd voom
cargo install --path .

Requires Rust 1.88+ (edition 2024).

Quick start

voom --dry-run ~      # see what's reclaimable across everything, without touching anything
voom ~                # actually clean it up — no confirmation prompt, see Safety
voom ./my-project     # clean one project

What a run prints:

/projects
  removed   4.20 GB  rust  api/target
  refused       0 B  rust  link/target    — is a symlink
  removed  18.50 MB  node  web/dist

2 artifacts, 4.22 GB reclaimed in 1.23s
  scan 900.00ms · policy 12.00ms · size 210.00ms · delete 98.00ms
  rust 4.20 GB (1), node 18.50 MB (1)
  1 artifact refused by a safety rail

Usage

Start narrow: voom --dry-run --verbose ~/projects, and read the skips before widening to ~.

voom --dry-run --verbose ~                                  # preview, with a reason for every skip
voom --min-age 7d ~                                          # only artifacts untouched for a week
voom --ecosystem rust --ecosystem node ~                     # only these ecosystems
voom --format json ~ | jq '.totals'                          # machine-readable
voom --list-caches                                           # the tool caches this machine holds
voom --dry-run --clean-caches ~                              # remove every proven tool cache
voom --dry-run --clean-caches=cargo-registry,uv ~            # remove these named tool caches
voom --dry-run --include .mytool-cache ~/projects            # sweep something the catalog misses
voom --dry-run --clean-dependencies ~                        # also node_modules/, vendor/, deps/, .venv/
voom --no-git ~                                               # skip the git housekeeping a sweep does
voom --summary --config ./ci-voom.toml ~/projects             # totals only, explicit config file

Run voom --help for the full, grouped list, or see the options reference.

Exit codes: 0 clean, 1 some artifacts could not be removed (a partial removal counts), 2 usage or config error, 3 findings with --dry-run --exit-code (for hooks). Git housekeeping never fails a sweep. catalog, --list-caches, config show and suggest always exit 0.

Options

Every flag and subcommand, with defaults and short forms, is in the reference. voom --help prints the same list grouped the same way.

Safety

voom deletes by default, with no confirmation prompt. Safety comes from these rules:

1. Nothing is removed without a marker file proving its ecosystem. target/ is only Rust's build output when a Cargo.toml sits beside it; obj/ is only .NET's when a project file — *.csproj, *.fsproj, *.vbproj or *.sln — sits at most three directories above it. A marker's position relative to the candidate is one of three anchors: Sibling (beside the artifact, most ecosystems), Ancestor(n) (up to n directories above, for artifacts nested inside a project), or Inside (the marker is written inside the candidate itself — ~/.cargo/registry's CACHEDIR.TAG, .basemind/'s agent-id). --verbose says which marker was missing.

2. Dependency directories are off by default. node_modules/, vendor/ (Rust, PHP composer, Go), mix's deps/ and Python's .venv/ are network-fetched caches, not build output — removing one costs a re-download and can break offline work. --clean-dependencies turns the group on and --enable <spec> turns on exactly one; a marker still has to prove each.

3. Tool caches and installed toolchains are skipped by location, not name — an installed pnpm sits beside a real package.json and is otherwise indistinguishable from a project someone built. The skip is unconditional: nothing re-opens those locations, though naming one of those paths as a scan root sweeps it anyway, because root intent outranks a walk heuristic.

4. Six rails you cannot switch off. Symlinks and Windows reparse points are refused rather than followed. Every target is canonicalized. An append-only denylist protects /, $HOME itself, /usr, /System, .git and friends. The result must resolve strictly below a scan root. Filesystem boundaries are not crossed (--one-file-system=false to opt out). A failure on one artifact is reported and isolated rather than aborting the run.

5. --dry-run is the real pipeline with the last step withheld, not a simulation.

Full reasoning in ADR 0002 and ADR 0006.

Cache catalog

The ecosystem catalog covers build output; it has nothing to say about ~/.cargo/registry or ~/.cache/uv, which are machine-global downloads rather than anything a project built. voom --list-caches lists the ones voom knows how to empty by name:

id Cache Location(s)
cargo-registry Cargo registry ~/.cargo/registry
cargo-git Cargo git checkouts ~/.cargo/git
uv uv ~/.cache/uv
gradle-caches Gradle caches ~/.gradle/caches
go-build Go build cache ~/Library/Caches/go-build, ~/.cache/go-build
go-mod Go module cache ~/go/pkg/mod
npm npm cache ~/.npm/_cacache
pnpm pnpm metadata cache ~/Library/Caches/pnpm, ~/.cache/pnpm
pub Dart pub cache ~/.pub-cache
deno Deno cache ~/Library/Caches/deno, ~/.cache/deno, ~/.deno
poly poly cache ~/.cache/poly, ~/Library/Caches/poly, ~/AppData/Local/poly
alef-global alef global cache ~/.cache/alef

The id is what --clean-caches and [caches] enable take — not always the tool's name, since a cache id and an ecosystem id sharing one word would leave a report unable to say which table a line came from. Each is proven the way everything else is: not by its path, but by a marker the tool wrote inside it, most often the cross-tool CACHEDIR.TAG. None is ever removed without that marker, and unless it lies under the tree being swept:

voom --list-caches                      # the caches on this machine, each with its size
voom --list-caches --verbose            # ... and every location, marker and note the table knows
voom --dry-run --clean-caches ~         # a bare flag: every proven cache under the root
voom --dry-run --clear-caches ~         # `--clear-caches` is the same flag, spelled that way
voom --dry-run --clean-caches=uv,go-build ~   # or the ones you name, comma-separated with `=`

By default --list-caches prints only the cache locations that actually exist here, each with its path and how much disk it holds; --verbose shows the whole table — every location the table knows about, whether it is present here, and the marker that would prove it.

A cache whose contents are only shard directories — sccache, zig, NuGet, Maven — is deliberately absent: the marker would prove nothing the path had not already said. See ADR 0012.

Tagged directories

A catalog entry matches a name and a cache entry matches a location. Neither can reach a build directory that has neither — a CARGO_TARGET_DIR pointed at /tmp, a .build-cache/ somebody renamed, a cache belonging to a tool voom has never heard of. Measured on one workstation, that was 104 GB across 53 directories.

--clean-tagged reaches them, using the one piece of evidence that needs no name and no location: the CACHEDIR.TAG the tool wrote inside the directory to say the contents can be rebuilt. tar --exclude-caching, rsync --exclude-tag, Borg and restic all honour the same file.

voom --dry-run --clean-tagged ~ /tmp   # read it first; this takes directories of any name

It is never on by default, and the reasons it is safe anyway are worth stating:

  • The signature is verified, not the filename. A file someone happened to call CACHEDIR.TAG does not license a removal.
  • An anchored artifact wins where both apply, so target/ beside a Cargo.toml is still reported as Rust's.
  • A tagged directory is pruned, not descended — which makes a sweep faster, since a relocated target directory is otherwise walked in full to report nothing.
  • A symlink is never probed, because reading through it is the one thing voom does not do.
  • Every keep policy, the protected denylist, containment and the filesystem boundary are unchanged.

Also settable as [tagged] enabled = true in a voom.toml, which is the useful form for a single project whose output goes somewhere the catalog cannot name. See ADR 0013.

Finding what the catalog does not cover

The catalog covers ecosystems, not the cache your own tooling writes into every package directory. include (ADR 0004) is the answer, but you have to know the directory's name to write the line. voom suggest walks a tree and reports the repeated directories git already ignores where they sit, ranked by size, with the line that would sweep each:

$ voom suggest ~/projects
     42.70 GB  .alef  across 69 directories
      3.00 GB  .basemind  across 13 directories

Add to voom.toml what you recognise:

  include = [".alef"]
  include = [".basemind"]

Read the list before pasting it. A .gitignore also lists local configuration and
scratch data, which is why voom suggests rather than sweeps.

It removes nothing. --include <GLOB> is the command-line equivalent of voom.toml's include key, for when you already know the name.

Supported ecosystems

Ecosystem Marker Artifacts
Rust Cargo.toml target/, vendor/†
Node / TypeScript package.json dist/, .next/, .nuxt/, .svelte-kit/, .astro/, .turbo/, .parcel-cache/, .vite/, .nyc_output/, *.tsbuildinfo, build/†, node_modules/†
Python pyproject.toml, setup.py, setup.cfg, BUILD, BUILD.bazel __pycache__/, .pytest_cache/, .mypy_cache/, .ruff_cache/, *.egg-info/, htmlcov/, .coverage, dist/, .tox/†, build/†, .venv/†
Go go.mod bin/†, vendor/†
Zig build.zig .zig-cache/, zig-cache/, zig-out/
Swift Package.swift .build/
Xcode *.xcodeproj, *.xcworkspace DerivedData/
Maven pom.xml target/
Gradle / Kotlin build.gradle, build.gradle.kts, settings.gradle* build/, .gradle/
.NET *.csproj, *.fsproj, *.vbproj, *.sln bin/, obj/
CMake / C++ CMakeLists.txt cmake-build-*/, CMakeFiles/, build/†
Dart / Flutter pubspec.yaml .dart_tool/, build/
Elixir mix.exs _build/, .elixir_ls/, deps/†
Ruby Gemfile, *.gemspec coverage/, vendor/bundle/†, pkg/†, tmp/†
PHP composer.json .phpunit.cache/, .phpunit.result.cache, vendor/†
Scala / sbt build.sbt target/, project/target/, .bloop/, .metals/
Haskell *.cabal, stack.yaml dist-newstyle/, .stack-work/
OCaml / dune dune-project _build/
Julia Project.toml deps/build/†
R DESCRIPTION *.Rcheck/, src/*.o, src/*.so
Nim *.nimble nimcache/
Elm elm.json elm-stuff/
Terraform *.tf .terraform/
Bazel WORKSPACE, WORKSPACE.bazel, MODULE.bazel bazel-bin/, bazel-out/, bazel-testlogs/, __pycache__/§, .pytest_cache/§, .mypy_cache/§, .ruff_cache/§
alef CACHEDIR.TAG .alef/‡
basemind agent-id .basemind/†‡

‡ Proven from inside: agent-id is the file basemind writes into .basemind/ when it creates the index, so the directory declares itself rather than relying on a sibling config that is usually absent. Every other row looks for its marker beside or above the artifact. A tool that wants its caches swept can do the same with the cross-tool CACHEDIR.TAG.

§ Proven by a WORKSPACE/WORKSPACE.bazel/MODULE.bazel anywhere above, with no level bound — these four are Python's own artifacts, declared a second time here for the tree that has no Python manifest anywhere near them, only Bazel's per-directory BUILD files. The Python row's Ancestor(1) still proves the common case faster; this is what reaches the rest. See ADR 0002's Anchor::WorkspaceRoot amendment.

† Off by default — the name is also a plausible source directory in that ecosystem, or removal is expensive rather than cheap. Turn one on for a single run with --enable python.venv, or per ecosystem and per path in your config. voom catalog prints the live table, with the reason each † entry needs an opt-in.

Configuration

voom merges built-in defaults, ~/.config/voom/config.toml, and every voom.toml from the scan root down to each candidate — nearest wins, so a repository can protect its own quirks and still be swept correctly from $HOME. Every table rejects unknown fields, so a typo is a hard load error rather than a guard that silently never took effect.

exclude = ["~/work/client-x/**"]   # never scanned, always wins
include = ["~/scratch/build-junk"] # swept without a marker — explicit intent

[keep]
min_age = "7d"                     # never remove something modified more recently

[caches]
enable = ["uv"]                    # opt into a named tool cache

Keep policies, per-ecosystem and per-path overrides, [ecosystems], [git] and the Windows quoting rule are in the configuration reference. voom config show [PATH] prints the merged result and the files it came from.

JSON output

--format json writes one object per run to stdout; diagnostics stay on stderr. It implies --verbose and suppresses the spinner. The shape is versioned and documented field by field in the reference.

voom --dry-run --format json ~ | jq '.totals'

Git housekeeping

An ordinary sweep also runs git's own local housekeeping in every repository it walks past — free, since the walker already meets .git on its way past:

Step What it does
git worktree prune --expire drops the administration of worktrees whose checkout has been gone for three months
git gc --auto repacks only if git's own thresholds say it is worth it

Neither contacts a network or expires a reflog beyond git's own policy, and voom never passes --prune=now, --aggressive, or git reflog expire. Hooks are disabled for every invocation. An in-progress rebase, merge, cherry-pick or bisect is skipped, and a linked worktree is resolved to its object store and pruned once rather than per checkout. The report says nothing when nothing was found:

  pruned  api  — 2 stale worktrees pruned

Turn it off with --no-git, or [git] enabled = false in a voom.toml (the flag wins). See ADR 0011.

voom git-prune runs the same housekeeping on its own, and is the only place the network step (--remotes) lives — one round trip per remote per repository, which hangs without connectivity and can block on a credential prompt, so it is deliberately not part of an ordinary sweep:

voom git-prune -n ~/projects              # what would be pruned
voom git-prune --remotes ~/projects       # also drop remote-tracking branches whose upstream is gone
voom git-prune --format json ~/projects   # machine-readable, same shape embedded under a sweep's "git" key

A repository whose housekeeping fails or times out makes git-prune exit 1; a skip is a rail doing its job and exits 0.

Bazel output-base housekeeping

Every workspace path that has run Bazel gets its own output base, and nothing removes it when the workspace goes away — not git worktree prune, which never sees it, and not the cache catalog above, which proves one fixed location rather than one of however many a user has pointed Bazel at. Bazel writes DO_NOT_BUILD_HERE into an output base's root naming the workspace that owns it, and that is proof enough to answer the one question that matters: is the owner still there?

voom bazel-prune                          # search the conventional locations
voom bazel-prune -n /var/tmp/_bazel_you    # what would be removed, without removing it
voom bazel-prune --format json             # machine-readable

Given no path, it searches /tmp/_bazel_<user>, /var/tmp/_bazel_<user> and ~/.cache/bazel/_bazel_<user>, and finds nothing at whichever do not exist on this machine — that absence is never a usage error, whether the root was defaulted or named. An output base whose recorded owner no longer exists anywhere on disk is removed; one whose owner still exists is left alone and named in the report regardless, since existence is not the same claim as "one of the workspaces this invocation was told about." Not part of a sweep — an output base never lives under the tree it was built from, so there is nothing to discover for free the way git-prune's repositories are. See ADR 0014.

Claude Code job scratch

~/.claude/jobs/<id>/tmp/ is free-form scratch a background job writes to, and nothing reaps it once the job is done. Nothing on disk says reliably whether a job is done, though — a state.json that says "stopped" can lag a job resumed later outside its own daemon by more than a week, which is exactly what motivated this command's caution.

voom claude-prune                     # report only — every job found, nothing removed
voom claude-prune --remove            # actually reclaim eligible jobs' scratch
voom claude-prune --remove -n         # what --remove would do, without doing it
voom claude-prune --min-age 30d       # a longer quiet period than the 14-day default

A job is eligible only when all hold: no process on this machine right now has its recorded session on its command line, it has been quiet longer than --min-age (14 days by default), and --remove was given. voom claude-prune with no flags at all only ever reports — read what it found, and decide. Only tmp/ is ever removed; state.json and a job's own record survive. See ADR 0015.

Watch mode

voom watch ~/projects

Prunes continuously as you work. Filesystem events are coalesced over a debounce window (--debounce, default 5s), and an artifact is only removed once its subtree has been idle for a quiet period (--quiet-period, default 60s), so an in-progress build is never touched. Watch mode never forces. See ADR 0008.

Git hooks

The reporting hook is the one to start with — it fails the commit on findings but deletes nothing.

pre-commit

repos:
  - repo: https://github.com/Goldziher/voom
    rev: v0.4.5
    hooks:
      - id: voom-report      # reports, deletes nothing
      # - id: voom-prune     # actually deletes — opt in deliberately

poly

[[hooks.sources]]
id = "voom"
git = "https://github.com/Goldziher/voom.git"
revision = "v0.4.5"
hooks = ["voom-report"]

Lefthook

pre-commit:
  commands:
    voom:
      run: voom --dry-run --report --exit-code .

Performance

voom is I/O-bound, so the wins come from not walking things: it prunes matched artifacts, .git/, dependency directories and tool caches at the directory level rather than descending into them, and fans sizing and removal across every core. -j throttles the whole sweep, not just the walk — the point of the flag on a spinning disk or a network filesystem. Measure your own tree with hyperfine; see ADR 0005 for the walk's design.

Development

cargo build
cargo test
cargo clippy --all-targets -- -D warnings
cargo fmt --all
poly hooks run pre-commit --all-files      # the authoritative gate

Design decisions live in adrs/ — start with 0001 and 0002. See CONTRIBUTING.md for adding an ecosystem, which is the most common contribution and has a checklist.

Contributing

Issues and pull requests are welcome, especially new ecosystems.

If voom is useful to you, consider sponsoring development.

License

MIT

About

Fast, safe, parallel polyglot build-artifact and cache pruning

Topics

Resources

Contributing

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages