Guide
Everything the command line tool does, in the order you are likely to need it. --help on any command prints the same thing without a browser.
First scan
A scan walks a folder and prints what it found. Nothing is stored, nothing is changed.
$ spacetrace scan ~/projects
scanned 84,213 entries in 1.9s
total 12.4 GiB logical · 12.7 GiB on disk
3 paths could not be readUnreadable paths are counted and sampled rather than stopping the walk. On macOS, reaching outside your home folder needs Full Disk Access for your terminal; without it the scan still completes and tells you what it missed.
Making it faster and narrower
spacetrace scan / -x spacetrace scan ~/code --exclude node_modules --exclude .git spacetrace scan /var --depth 3 spacetrace scan /srv --no-dedupe
--exclude takes a directory name, not a path, and repeats. Excluded folders are never descended into, so excluding node_modules on a developer machine is usually the difference between two seconds and thirty.
Snapshots and diff
This is the part other analysers do not have. Save a scan, save another one later, and ask what changed between them.
$ spacetrace scan /srv --save --label weekly saved snapshot #1 $ spacetrace scans ID WHEN HOST ROOT LABEL TOTAL 2 2026-09-06 19:40 srv-01 /srv 76.3 MiB 1 2026-09-06 17:23 srv-01 /srv weekly 23.8 MiB
Then compare. With no arguments it takes the last two.
$ spacetrace diff --path /srv
#1 2026-09-06 17:23 [weekly] → #2 2026-09-06 19:40
total 23.8 MiB → 76.3 MiB (+52.5 MiB)
CHANGE STATUS NEW PATH
+40.1 MiB grew 42.9 MiB app/logs/
+14.3 MiB grew 25.7 MiB backups/
-1.9 MiB removed 0 B uploads/A folder whose growth comes entirely from one child tells you nothing you did not already know, so it is skipped. The report names the first level where the change genuinely spreads out — the culprit, not its ancestors.
Against the disk as it is now
You do not need two snapshots. Compare the newest one against a live scan:
spacetrace diff --since-last /srvChoosing what appears
spacetrace diff --path /srv --min 10M spacetrace diff --path /srv --files spacetrace diff 3 7
Keeping the database from growing forever
spacetrace prune --path /srv --keep 30 spacetrace rm 4
Browsing a scan
ls lists folders by size, either from a live scan or from a stored snapshot.
spacetrace ls /var/lib --top 20 spacetrace ls --scan 3 --subpath docker/overlay2
To hand a snapshot to something else, export writes ncdu's own format:
spacetrace export --scan 3 --out scan.json && ncdu -f scan.jsonEvery command also takes --json, which is the supported way to script against it. The human-readable columns are free to change; the JSON shape is not.
The agent on a server
spacetrace-agent is the same code as a service: it scans the roots you configure on a schedule, keeps the snapshots, and answers over HTTP. It is one static binary, and it only ever reads — there is no code path in it that deletes anything outside its own snapshot database.
Write a config and a token
spacetrace-agent init > /etc/spacetrace/agent.tomlhead -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' > /etc/spacetrace/token && chmod 600 /etc/spacetrace/tokenserve refuses to start without a token. An unauthenticated agent hands its whole filesystem inventory to anyone who can reach the port.
Say what to scan and when
db = "/var/lib/spacetrace/snapshots.sqlite" utc_offset_minutes = 0 [server] listen = "127.0.0.1:7878" token_file = "/etc/spacetrace/token" [[roots]] path = "/var" schedule = "0 3 * * *" label = "nightly" exclude = ["node_modules", ".git"] one_file_system = true keep = 14
Five cron fields, with *, a-b, */n and lists. No seconds, no @daily. Unknown config keys are rejected at startup, because a typo that silently does nothing on a machine nobody watches is worse than a refusal to boot.
Check before you start it
$ spacetrace-agent --config /etc/spacetrace/agent.toml check config ok database /var/lib/spacetrace/snapshots.sqlite host nas listen 127.0.0.1:7878 token configured ad-hoc scans refused ROOT SCHEDULE NEXT RUN /var 0 3 * * * 2026-09-09 03:00 Times are UTC.A schedule that can never fire reports never rather than failing quietly.
Run it
systemctl enable --now spacetrace-agentThe bundled systemd unit runs the agent as its own unprivileged user under ProtectSystem=strict, at Nice=10 with idle I/O priority. A scan should never get in the way of what the machine is actually for.
In Docker, mount the host read-only and scan that:
docker run -d --name spacetrace \ -v /:/host:ro \ -v spacetrace-data:/var/lib/spacetrace \ -v /etc/spacetrace:/etc/spacetrace:ro \ -p 7878:7878 \ ghcr.io/unalcakir28/spacetrace
Before you expose it
- It binds loopback by default. Publishing a filesystem inventory to a network should be a deliberate edit.
- There is no TLS in the agent. Put a reverse proxy in front of it.
- The token is compared without an early exit, so a wrong token takes the same time to reject however much of it was right.
- /health needs no token so a container healthcheck works, and returns only status and version — no hostname, no roots.
- Ad-hoc scans are off. With them on, anyone holding the token can enumerate any directory the agent's user can read.
Reading another machine
Any read-only command takes --remote. Nothing new has to be learned: the same subcommands, pointed elsewhere.
export SPACETRACE_TOKEN=… spacetrace --remote https://nas.example.com scans spacetrace --remote https://nas.example.com diff --path /var spacetrace --remote https://nas.example.com ls --top 20 spacetrace --remote https://nas.example.com pull --root /var
Save a remote so the URL and token stop being typed — in ~/.config/spacetrace/remotes.toml:
[remotes.nas] url = "https://nas.example.com" token = "…"
What comes down the wire is the same standalone SQLite file the agent stores, so listing, browsing and diffing it run the identical code as a local snapshot. You can skip the tool entirely and still get a file you can open:
curl -H "Authorization: Bearer $SPACETRACE_TOKEN" \ https://nas.example.com/scans/7/download -o snap.sqlite spacetrace --db snap.sqlite scans
scan, prune and rm refuse to run with --remote: they act on local state, and the agent deletes nothing.
Which size, and why
Every entry carries two numbers, and neither is an estimate of the other.
| Measure | What it is | Matches |
|---|---|---|
| logical | The length each file reports | du -sb |
| on disk | Blocks actually allocated, directory blocks included | du -s --block-size=1 |
| capacity | Free space out of total, on the scanned filesystem | df's Avail column, exactly |
They diverge in both directions and both are right. A one-byte file allocates a whole block, so it is bigger on disk than its length. A sparse file reports a length it never allocated: a disk image can claim a terabyte and hold nineteen gigabytes.
Sparse files are why the app defaults to on disk. Virtual machine images, database files and core dumps are among the largest entries on any real disk, so the logical measure is most wrong about exactly the entries that matter most. The command line defaults to logical, and both say which one they are showing.
Capacity is reported as free of total, never as “% used”. On a filesystem whose space is shared between volumes — an APFS container, btrfs subvolumes, thin LVM — a used figure would include the siblings and disagree with df about the same mount.
Hardlinks are counted once by default; the second copy appears in the tree contributing zero bytes. Symlinks are never followed and count at their own size.
Command reference
- spacetrace scan PATH
- --save, --label
- spacetrace scans
- —
- spacetrace diff
- --path, --since-last, ID ID
- spacetrace ls PATH
- --top, --scan, --subpath
- spacetrace export
- --scan, --out
- spacetrace pull --root PATH
- --remote
- spacetrace prune / rm
- --keep, ID
- spacetrace-agent init
- —
- spacetrace-agent check
- —
- spacetrace-agent serve
- —
- spacetrace-agent push URL
- --root, --token
Options that work on most commands
- --exclude NAME
- -x, --one-file-system
- --depth N
- --min SIZE
- --files
- --no-dedupe
- --json
- --db PATH
- --remote NAME|URL
Where things are kept
| Platform | Snapshot database |
|---|---|
| macOS | ~/Library/Application Support/spacetrace/ |
| Linux | $XDG_DATA_HOME/spacetrace/ |
| Anywhere | SPACETRACE_HOME overrides it; --db overrides that for one command |
The desktop app writes the same database, so a snapshot saved in the app appears in spacetrace scans and the other way round.
Known limits
Published rather than discovered. If you hit one of these it is known debt, not a surprise.
- On Windows, on-disk size equals logical size and hardlink de-duplication is off. Real figures need GetFileInformationByHandleEx and FileIdInfo.
- APFS clones are not de-duplicated, and on btrfs or ZFS reflinks and compression mean a tree walk cannot report true usage. No tree walker can; the numbers are the files' own.
- A scan holds the whole tree in memory. The memory profile above ten million files has not been measured.
- The agent has no TLS and no rate limiting. Use a reverse proxy; the token is already required.
- The scheduler has no timezone database — UTC plus a fixed offset, so daylight-saving shifts are yours to think about.