Smart span. Free disks.
Spanwit helps to keep your disks free for the important work you do.
Smart, signature-aware disk reclamation that knows when a target/ folder is
fair game and when it isn't. Dev-aware today, fleet-aware tomorrow. Dry-run by
default.
Disks fill up. A Rust monorepo, a few JS workspaces, a Go module cache or
two, and suddenly you're scrolling df -h wondering what's safe to remove.
Spanwit reads your disk the way a developer would — recognizing that a
target/ next to a Cargo.toml is fair game, but a target/ somewhere else
may not be. Smart rules, dry-run by default, composable in a pipeline today
and governable across a fleet tomorrow.
Etymology: span (capacity, free space) + wit (Old English witt —
intelligence, awareness). The benefit and the mechanism, in one word.
Dry-run is enabled by default. Nothing is deleted without an explicit
--execute flag. There is no interactive prompt; --execute is the explicit
opt-in, and safety and policy guardrails still apply.
This tool follows the Fulmen microtool pattern: narrow scope, excellent defaults, extremely safe operation.
Primary use cases:
- Find and safely remove large Rust
target/directories - Clean stale
node_modules, build artifacts, caches - Reclaim space on dev machines without accidental data loss
# Safe read-only analysis (default)
spanwit scan ~/dev
# Neutral file inventory (read-only; size/age never imply safe-to-delete)
spanwit --read-only inventory ~/dev --directories --directory-min-size 5GiB
spanwit inventory ~/dev --min-size 1GiB --older-than 30d --top 100
spanwit inventory ~/dev --min-size 1GiB --format jsonl
spanwit inventory ~/dev --summary-only --exclude node_modules
spanwit inventory ~/dev --summary-only \
--coverage-attest /Volumes/diagnostics/dev-inventory-coverage.json
# File-level diagnostics (read-only; found-only classification)
spanwit diagnose ~/Downloads
# Why is the disk full? (read-only; never deletes)
spanwit space ~/dev
spanwit space ~/dev --capacity-only --containers --probe-vm
spanwit space ~/dev --format json | jq .
# Tune observation walks (verified discovery and prune handoff are independent)
spanwit space ~/dev --workers 4 --stall-timeout 60s
# Fast capacity planes only: filesystem, pool/volumes, snapshots, managed roots
spanwit space ~/dev --capacity-only
spanwit space ~/dev --capacity-only --format json > /tmp/capacity-report.json
# Observe registered volumes over time (propose-only; never deletes)
spanwit observe register "$HOME"
spanwit observe once --format json
spanwit observe status --foreground
# Plan a prune (dry-run); add --execute to actually delete
spanwit prune --config ~/.config/spanwit/config.yaml
spanwit prune --config ~/.config/spanwit/config.yaml --execute
# Machine-readable plan for pipelines
spanwit prune --format json | jq .Capacity ritual (sense → handoff → reclaim → optional recipes):
# 0. When named diagnostics do not explain the pressure, enumerate large files.
# This is neutral evidence, not deletion authority.
spanwit inventory /System/Volumes/Data --min-size 5GiB --older-than 30d \
--one-filesystem --top 200
# Or capture allocation planes without a deep inventory walk.
spanwit space ~/dev --capacity-only --format json > /tmp/capacity-report.json
# 1. Triage — actionability layout; filter to what you can free now
spanwit space ~/dev --min-size 500M --class prunable --sort idle
# Named caches / toolchains only (diagnostic-only; never deleted by spanwit)
spanwit space ~/dev --class diagnostic-only --domain development
# 2. Save JSON (carries prune_handoff.exact_plan + applied_filters when set)
spanwit space ~/dev --min-size 500M --class prunable --format json > /tmp/space-report.json
# 3. Revalidate the exact plan (dry-run), then execute only after it matches
spanwit prune --from-space-report /tmp/space-report.json
spanwit prune --from-space-report /tmp/space-report.json --execute
# 4. Optional: guided recipes in space text output (e.g. rustup, VM disk trim,
# grype download residue). Never auto-run.Bare --allowlist is a dry-run convenience against built-in signatures only and
refuses --execute (no custom-signature/filter provenance). Prefer
--from-space-report for policy-preserving reclaim.
inventory requires at least one explicit root and creates no report, cache,
index, or temporary file by default. Without --top, matches stream in
unspecified traversal order. --top N scans the complete declared scope and
retains only a bounded ranking. --format jsonl emits typed
spanwit.inventory.*.v1 records under the producer profile
spanwit.filesystem-inventory/v0; --summary-only suppresses entries without
weakening coverage, and repeatable --exclude rules define the declared
subject. --coverage-attest PATH optionally writes a separate, validated,
block-export producer coverage claim with no overwrite; it never enters the
inventory stream. --print0 emits local paths only. None of these projections
authorize deletion.
inventory --directories is a readable folder audit: allocated size by default
and an inclusive --directory-min-size floor over the full-subject walk.
--format jsonl emits the versioned profile
spanwit.filesystem-inventory-aggregation/v2, and --coverage-attest attests
traversal of the full subject. See docs/inventory.md.
space --capacity-only uses one filesystem observation for both pressure and
the filesystem capacity plane, then attempts the optional analysis-root sample
and platform-specific read-only accounting under one shared five-second context
budget. Add --held-open for a separate, slower read-only Darwin pass over
unlinked files that remain open:
spanwit space --capacity-only --held-open \
--session-boundary before-running --format json > before.json
spanwit space --compare before.json after.jsonHeld-open objects are deduplicated by device and inode. Their logical sizes are
lower-bound diagnostic evidence, never reclaimable or APFS allocated-byte
claims; Spanwit does not stop processes or elevate privileges. Paths are absent
by default, including basenames. --disclose paths is an explicit report-file
disclosure choice.
space --containers adds container engine storage headroom: how full the
engine's data disk is and what fills it. It reads the Docker Engine API on the
local socket (read-only; no docker CLI needed) and works with full and
--capacity-only captures:
spanwit space --capacity-only --containers --probe-vmThe section reports the engine (colima, Docker Desktop, OrbStack, podman,
native Linux), storage by kind (images with unique and shared bytes, build
cache, dangling images, images for a foreign platform, superseded semver tags,
unreferenced volumes) and a relief list ordered by cost: rebuild, re-pull, then
data-loss risk, followed by a guest trim. Spanwit never runs these steps.
--probe-vm measures the VM's data disk from inside (colima ssh -- df);
without it the guest figure is not_probed. Image refs, volume names, VM
profiles and socket paths are not reported; --image-refs lists refs for local
review only, since refs can carry private registry names. Two captures with the
section feed space --compare for a typed before-and-after.
space --compare LEFT RIGHT reads exactly two explicit SpaceReport v2 captures
(one may be - for stdin), validates them as untrusted bounded input, and emits
only claim-compatible right-minus-left deltas. Capture-mode and boot changes are
informational; missing or mismatched volume/container identity stays
plane-local and indeterminate/incomparable. No report database, cache, daemon
state, reconciled total, or numeric “unexplained” residual is used.
Optional planes report
measured, partial, unsupported, or unavailable; missing values are not
reported as zero and planes are never summed into an invented reconciled total.
On macOS it uses public diskutil -plist interfaces and measures only
catalog-constrained system roots as diagnostic-only lower bounds. Held-open
collection remains not_requested unless its flag is set. Capacity-only reports
are deliberately rejected by prune --from-space-report.
Before sharing a capacity report, review its ordinary analysis root, mounts,
catalog-constrained system roots, and APFS volume names. A user-renamed volume
name can contain personal or organizational text even when held-open path
disclosure is none.
Filesystem sampling checks the shared context before and after each native statfs-style call. Because supported kernels do not universally provide an interruptible statfs syscall, that syscall boundary is best-effort rather than a hard wall-clock cancellation guarantee; command runners and managed-root enumeration are context-cancelable.
See docs/inventory.md for filter, backend, coverage, output, and privacy semantics.
Configuration resolves in order: --config flag, then SPANWIT_CONFIG_PATH,
then the default ~/.config/spanwit/config.yaml. Example profiles (crisis vs
maintenance) and the full crisis recipe live in
docs/configuration.md.
Prebuilt, signed binaries are published for macOS (Apple Silicon), Linux (amd64, arm64) and Windows (amd64, arm64).
# Homebrew (macOS Apple Silicon, Linux)
brew install 3leaps/tap/spanwit# Scoop (Windows)
scoop bucket add 3leaps https://github.com/3leaps/scoop-bucket
scoop install spanwitOr download an archive from
GitHub Releases and verify
SHA256SUMS with the published minisign or PGP key. From source:
go install github.com/3leaps/spanwit/cmd/spanwit@latestgo install builds without release version metadata, so spanwit version
reports dev.
make build # builds bin/spanwit
make pr-final # full non-mutating quality gatePhase 1 (now): a CLI you run after a grumpy df -h.
Phase 2: a low-overhead background agent with continuous capacity awareness (spanwitd).
Phase 3: a small HTTP control surface for governing reclamation policy across
a fleet of dev boxes, build agents, and CI runners ("Spanwit Fleet").
The signature engine — the thing that knows a target/ next to Cargo.toml
is regenerable but a target/ elsewhere may not be — is the same engine that
will generalize to ML workstations (model artifacts, dataset caches),
data-engineering boxes (stale checkpoints, materialized views), and CI/build
farms.
v0.2.2 — alpha. Notes: docs/releases/v0.2.2.md.
Dry-run by default. space, inventory and diagnose are
read-only (--coverage-attest additionally writes a separate attestation
file). observe keeps local state and is propose-only: it never deletes, but
it writes, so --read-only rejects it.