cruftkill
v0.4.3
Published
Polyglot dev-cache reaper — find and delete node_modules, .venv, target, DerivedData and the rest of your build cruft from a fast terminal UI
Maintainers
Readme
cruftkill (cft)
██████╗███████╗████████╗
██╔════╝██╔════╝╚══██╔══╝
██║ █████╗ ██║
██║ ██╔══╝ ██║
╚██████╗██║ ██║
╚═════╝╚═╝ ╚═╝Polyglot dev-cache reaper. Find and delete node_modules, .venv,
target, DerivedData, __pycache__, obj, .gradle, .next,
.turbo and the rest of your build cruft from a fast terminal UI.
Inspired by voidcosmos/npkill — but rewritten in Rust with a parallel async scanner and extended to 17 ecosystems.
⚠️
cftdeletes recursively without a recycle bin. Always review the list before pressingd. Run with--dry-runfirst if you want to preview.
Visuals
Interactive TUI Mode

Web Admin Dashboard Mode

Features (v0.4)
- 🌐 17 hardcoded ecosystem profiles — node, python, rust, java, swift,
dotnet, ruby, elixir, haskell, scala, cpp, unity, unreal, godot, infra,
data-science (or pass
--profile all) - 🔍 Parallel async directory scanner with
CancellationToken - 📏 True on-disk size per folder (Unix
blocks × 512) - 🛡️ Risk analyzer — flags paths inside
~/.config, AppData,.appbundles - 🧭 Per-result metadata — ecosystem, cleanup category, delete-risk verdict, and rebuild hint under each scanned path
- 🗑️ Safe delete with two-layer guard (basename + canonicalize containment)
- 🖥️ Three UX modes:
- Interactive TUI (ratatui): navigate, sort by size/name/last-used, inspect cleanup metadata, delete with confirm, rescan
--no-tuimode: streams NDJSON for scripting / CI pipelinescft uiweb admin — localhost browser dashboard with live SSE streaming, bulk delete, saved scan presets, and a history page with "GB reclaimed over time" + "by ecosystem" charts. All destructive operations route through the same 2-layer guard as the TUI.
Install
From crates.io:
cargo install cruftkill
cft --helpFrom source:
git clone https://github.com/xuanphamdev/cruftkill
cd cruftkill
cargo install --path .Requires Rust 1.85+ (edition 2024).
Usage
Interactive TUI
cft # scan current dir with `node` profile
cft ~/Projects # scan a specific directory
cft -p rust ~/code # use the `rust` profile (matches `target/`)
cft -p node -p python ~/code # combine profiles
cft -p all ~/ # everything everywhere
cft --dry-run ~/Projects # preview what would be deletedEach result shows a metadata line under the folder path:
/work/app/.venv 1.4 GB 3w
data-science+python | virtual env | risk: low | Recreate with the project setup or package manager commandKeybinds:
| Key | Action |
|---|---|
| ↑ / k | move cursor up |
| ↓ / j | move cursor down |
| d / Space / Enter | open delete confirm |
| y / Y | confirm delete |
| n / N / Esc | cancel modal |
| s | toggle sort by size (desc default) |
| n | toggle sort by name (asc default) |
| m | toggle sort by last-used (desc default) |
| r / F5 | rescan |
| q, Ctrl-C | quit |
Scriptable JSON output
cft --no-tui ~/Projects | jq '. | select(.size_bytes > 100000000)'Each line is a JSON object:
{
"path": "/home/me/proj-a/node_modules",
"size_bytes": 314572800,
"is_sensitive": false,
"risk_reason": null,
"target_name": "node_modules",
"ecosystems": ["node"],
"category": "dependency-tree",
"delete_risk": "low",
"delete_risk_reason": "Regenerable cache or build output outside sensitive paths",
"rebuild_hint": "Reinstall dependencies before next build",
"modified_unix": 1779867789,
"dry_run": false
}Metadata is advisory. Delete confirmation and the safety guards still decide what can actually be removed. If risk analysis is disabled, metadata uses a medium-risk advisory because sensitive-path checks were skipped.
When stdout is not a TTY (e.g. piped or in CI), cft auto-falls-back to
--no-tui mode.
Web UI (cft ui)
cft ui # bind 127.0.0.1 on a random free port,
# auto-open the browser
cft ui --port 8080 # pin a specific port
cft ui --no-open # serve only; don't launch the browserA single-binary localhost admin — no Node toolchain, no separate static
bundle, no network listener beyond 127.0.0.1. Press Ctrl-C in the
terminal to stop the server gracefully.
The dashboard:
- Form-driven scan launch with profile chips + custom-target/exclude inputs.
- Live results table — each match streams in via Server-Sent Events
from a
broadcast::Senderfan-out, so multiple browser tabs can watch the same scan in real time. - Inline sort (size / name / age, asc + desc) + 300ms-debounced substring filter — re-rendered by the server, swapped in via HTMX.
- Per-row delete with a confirm modal that shows the risk verdict.
- Bulk select + bulk delete (live "N selected — SIZE reclaimable" summary bar, single-confirm modal for the whole batch).
- Saved presets — reusable profile+targets+exclude triples; click Run to pre-fill the dashboard form.
- History page — paginated past scans, plus two Chart.js panels: reclaimed bytes per day (line) and reclaimed bytes per ecosystem (donut). Each scan row drills into a full result list.
Persistence: scan history + presets live in a SQLite DB at
dirs::cache_dir()/cruftkill/cft.db (e.g. ~/Library/Caches/cruftkill/cft.db
on macOS, ~/.cache/cruftkill/cft.db on Linux,
%LOCALAPPDATA%\cruftkill\Cache\cft.db on Windows).
Safety model: every destructive op — single delete, bulk delete, drill-down
delete — routes through core::safe_delete (basename guard) +
core::delete (canonicalize + containment). Same 2-layer guard the TUI
uses; the web UI never bypasses it.
Profiles
Run cft --help or read src/core/profiles.rs for
the full target list. Highlights:
| Profile | Matches |
|---|---|
| node (default) | node_modules, .npm, .pnpm-store, .next, .nuxt, .turbo, .cache, coverage, … |
| python | __pycache__, .pytest_cache, .venv, .tox, .mypy_cache, … |
| rust | target |
| java | target, .gradle, out |
| swift | DerivedData, .swiftpm |
| dotnet | obj, TestResults, .vs |
| cpp | CMakeFiles, cmake-build-debug, cmake-build-release |
| unity | Library, Temp, Obj |
| unreal | Intermediate, DerivedDataCache, Binaries |
| godot | .import, .godot |
| data-science | .ipynb_checkpoints, .dvc, .mlruns, … |
| infra | .serverless, .vercel, .netlify, .terraform, … |
| all | union of every profile |
Add ad-hoc targets with -t:
cft -p node -t my_custom_cache ~/codeSafety model
Two layered guards run before any FS mutation:
- Basename guard — the path's basename must appear in the resolved target list. Catches the "wrong path" mistake.
- Containment guard — both the scan root and the target path are
canonicalized (symlinks resolved). The canonical target must
starts_withthe canonical root. Catches symlink escape attacks even if the link is named like a target.
std::fs::remove_dir_all is hardened against symlink-traversal
(CVE-2022-21658)
— Rust does not follow symlinks when removing a directory.
Architecture
cft binary (src/main.rs)
│
├── (no subcommand) ── scan mode
│ ├── TUI ──► src/tui/ (ratatui + crossterm + tokio::select!)
│ └── --no-tui ──► run_no_tui → NDJSON
│
└── `cft ui` ──► src/web/ (axum + Askama + HTMX + SQLite)
├── server.rs bind 127.0.0.1:port + graceful shutdown
├── state.rs AppState (db + active scan + broadcast)
├── db.rs rusqlite + spawn_blocking + schema
├── render.rs shared row + table templates
├── routes/ {pages, scans, sse, results, presets, history, assets}
└── assets/ embedded HTMX + HTMX-SSE + Chart.js + CSS
All three modes call into:
src/core/
├── scanner (parallel tokio worker pool, unbounded dispatch)
├── size (refcounted async sum, 60s timeout)
├── risk (pure path classifier)
├── metadata (ecosystem/category/delete-risk labels)
├── safe_delete (basename guard)
├── delete (canonicalize + remove_dir_all)
├── profiles (17 profiles, resolve_targets)
├── sort (path / size / age comparators)
├── filter (case-insensitive substring)
├── ignore (GLOBAL_IGNORE set)
├── types (ScanOptions, FolderResult, …)
└── error (CruftError)Templates live at the crate-root templates/ directory (Askama default).
See docs/architecture.md for the full layout.
Development
cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all -- --check
cargo build --releaseHistory
This crate was originally published as
nodemoduleskiller v0.1.0
(binary nmk). It was renamed to cruftkill (binary cft) in v0.2.0
because the scope had outgrown "node_modules only" — the tool now wipes 17
different language ecosystems' build cruft from a single command. The
GitHub repository was renamed too; the old URL xuanphamdev/nodemoduleskiller
auto-redirects to xuanphamdev/cruftkill.
Third-party embedded assets
cft ui ships with a small set of client-side libraries vendored into the
binary. Full attribution + license texts live in NOTICE. Summary:
- HTMX (2.0.4) — 0BSD
- HTMX SSE extension (2.2.2) — 0BSD
- Chart.js (4.4.6) — MIT
Attribution
This project was inspired by — and ported from — voidcosmos/npkill (© voidcosmos, MIT license). Many design decisions (target detection rules, risk-analysis heuristics, profile definitions, behavioural invariants) are preserved verbatim.
License
MIT — see LICENSE.
