spacetrace
EN
Download

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 read

Unreadable 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/
Why it names the deepest folder

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 /srv

Choosing 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.json

Every 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.

  1. Write a config and a token

    spacetrace-agent init > /etc/spacetrace/agent.toml
    head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' > /etc/spacetrace/token && chmod 600 /etc/spacetrace/token

    serve refuses to start without a token. An unauthenticated agent hands its whole filesystem inventory to anyone who can reach the port.

  2. 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.

  3. 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.

  4. Run it

    systemctl enable --now spacetrace-agent

    The 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 = "…"
A remote snapshot is not a different kind of thing

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.

MeasureWhat it isMatches
logicalThe length each file reportsdu -sb
on diskBlocks actually allocated, directory blocks includeddu -s --block-size=1
capacityFree space out of total, on the scanned filesystemdf'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

PlatformSnapshot database
macOS~/Library/Application Support/spacetrace/
Linux$XDG_DATA_HOME/spacetrace/
AnywhereSPACETRACE_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.

docs/AGENT.md