A macOS disk scanner that tells you what you can delete — and cannot delete it for you.
Every disk tool can tell you that ~/Library is 84 GB. That is not useful:
you cannot delete ~/Library. diskscan walks your disk in parallel and
produces one self-contained HTML file with a navigable treemap, the largest
files, and — the point of the whole thing — a list of specific folders you
can actually delete, each with the number of bytes you would really get
back and a button that opens it in the Finder.
Then it stops. There is no delete key. That is deliberate.
The generated report: navigable treemap on the left, and the "what to delete" tab on the right with reclaimable candidates, each explaining why it is safe.
$ diskscan ~
diskscan - scanning /Users/berna (24 threads)
1,284,331 files - 198,442 dirs - 248 GB - 11.4s
report: diskscan-report.html (4.7 MB)
> live report: http://127.0.0.1:52341/ (Ctrl-C to quit)git clone https://github.com/bernatch22/diskscan
cd diskscan
cargo install --path . # puts `diskscan` in ~/.cargo/bindiskscan # scan /, write the report, open the browser
diskscan ~ -o reports/home.html
diskscan / --no-serve # just the static HTML (buttons copy paths)
diskscan --report old.html # re-open a report without rescanning
sudo diskscan / # also see what your user cannot readAll options
| flag | meaning |
|---|---|
-o, --out <file> |
HTML to write (default ./diskscan-report.html) |
-x, --exclude <path> |
do not descend into this directory (repeatable) |
--all |
drop the default exclusions applied when scanning / |
--top <n> |
how many largest files to list (default 400) |
--port <n> |
port for the local server (default: random) |
--no-serve |
only write the HTML, do not start the server |
--no-open |
do not open the browser |
--threads <n> |
scanning threads (default 2× CPUs, capped at 32) |
--report <html> |
do not scan: serve a report written earlier |
Disk analysers split into two families. TUI scanners (ncdu, dust, gdu,
dua) are fast and live in the terminal, but what you learn dies with the
session — and most of them delete on a keypress. GUI treemaps (DaisyDisk,
GrandPerspective, OmniDiskSweeper) are beautiful and leave you staring at a
rectangle labelled Library, still guessing.
diskscan sits in neither camp, on four axes at once:
| diskscan | |
|---|---|
| What you get | one self-contained HTML file — mail it, keep it, open it in six months with nothing installed |
| What it measures | allocated bytes by default: what the volume actually loses |
| What it tells you | named, leaf-level folders that are safe to delete, not "your home folder is big" |
| What it can do to your files | nothing. There is no delete path in the codebase |
A size here is st_blocks * 512 — allocated blocks, not st_size. A 40 GB
.dmg that is 90 % sparse counts as what it occupies. Most tools report
apparent size by default and offer allocated size as an opt-in flag; here it
is the only mode, because a number you have to remember to ask for is a number
that will mislead you at some point.
One honest limit: APFS clones (cp -c, Finder duplicates, some Xcode
and Time Machine copies) share blocks on disk but st_blocks reports the
full allocation for each copy, so a pair of clones counts twice. Detecting
that needs per-file extent walks and is not implemented.
APFS firmlinks make /Users and /System/Volumes/Data/Users the same
directory. A naive walk counts your entire home folder twice — ncdu's own
manual warns that scanning the root without --exclude-firmlinks may count
directories multiple times and can loop until it runs out of memory.
diskscan deduplicates directories by (dev, ino), exactly as it
deduplicates hard-linked files, so a firmlink is recognised as somewhere it
has already been. The other APFS volumes in the container (Preboot, VM,
Update), /Volumes and /dev are excluded by default, since none of them
is space you can free. --all turns the exclusions off.
The walk uses macOS's getattrlistbulk(2), which returns name, type,
device, inode, link count and allocated size for hundreds of entries per
call. The usual readdir + lstat shape costs one syscall and one path
lookup per file; on a million-file home directory that difference is the
runtime. Volumes that do not implement the bulk call (network mounts, FAT,
some FUSE filesystems) fall back to readdir + lstat automatically, and a
test asserts the two backends agree.
Recursion is rayon work-stealing — the right primitive for a tree whose
branches differ in size by four orders of magnitude. Memory stays bounded:
only the directory tree survives the walk, plus three filenames per directory
and one global heap of the --top largest files.
ncdu and dua delete on a keypress. diskscan will not, ever — an earlier
version had a "move to Trash" button and it was removed, on purpose.
Deleting is a decision, and the Finder is a much better place to make it: it
shows you Quick Look previews, and it has Undo. So the report's buttons call
open and open -R, and that is the entire set of side effects available to
it. When the page is opened as a plain file with no server, they degrade to
"copy path".
The optional bridge that makes those buttons work is a ~200-line loopback server with five independent limits, all of which must hold:
- binds
127.0.0.1only; - every action carries a 128-bit token from
/dev/urandom, generated per run and embedded only in the served copy of the page — so another site your browser has open, which can guess the port but not the token, gets a 403; - the
Hostheader must name loopback, so a DNS-rebound page cannot read the token from its own origin; - paths must be absolute, free of
.., inside the directory that was actually scanned — and still inside it once symlinks are resolved, becauseopenfollows them; - the only two verbs are
openandopen -R, invoked asexecvearguments — never through a shell, so a file named; rm -rf ~is just a filename.
The HTML written to disk never contains a token. --no-serve skips the
server entirely.
One file, no CDN, no network access, works offline forever.
-
Treemap — squarified, canvas-drawn. Click to enter, double click to open in the Finder, right click to reveal,
⌫to go up,/to filter. -
Contents — children of the current folder by weight, with the three largest files at each level.
-
Largest files — the top N of the whole scan, filtered to wherever you are standing.
-
What to delete — the tab that does the actual work. Two lists:
- Known caches and data: a curated catalogue matched against real paths —
node_modules, XcodeDerivedDataand simulator runtimes,.npm,.cache,.cargo/registry, Homebrew, Docker/colima/lima/OrbStack, Android AVDs,__pycache__, virtualenvs, build output — each tagged regenerates or check first, with the command that cleans it properly. - Folders heavy with their own files: directories where the weight is in the files right there rather than spread across subfolders. What you see is what you free.
Candidates nested inside other candidates are dropped, so you are shown the
node_modules, not the project and itsnode_modules. - Known caches and data: a curated catalogue matched against real paths —
A 500 GB scan produces roughly 5 MB of HTML. That is the pruning at work: every directory keeps its 12 largest children plus anything over 1/20000 of the total, and the rest is folded into one "N smaller folders" row that still carries its byte count — pruned space is summarised, never silently dropped.
If the footer says N folders could not be read, grant your terminal Full Disk Access in System Settings → Privacy & Security, or run under
sudo.
macOS, Rust 1.74+. The crate will not compile elsewhere — it depends on
getattrlistbulk, APFS firmlink semantics and the Finder. A Linux backend
would be a welcome contribution.
Two dependencies: rayon and libc. No serde, no clap, no HTTP crate. For a
tool people are invited to run as sudo diskscan /, a dependency tree you can
read in an afternoon is a feature.
The binary is a thin shell over a library you can use directly:
use diskscan::scan::{self, Walk};
use std::path::Path;
let walk = Walk::new(Vec::new(), 400);
let tree = scan::scan_root(Path::new("/tmp"), &walk);
println!("{} bytes in {} files", tree.size, tree.files);| module | responsibility |
|---|---|
scan |
the parallel walk and its accounting rules |
report |
pruning, JSON, and HTML assembly |
serve |
the localhost Finder bridge |
cli |
argument parsing |
cargo test # 58 tests, including a real fixture tree
cargo clippy --all-targets -- -D warnings
cargo fmt --checkThe integration suite in tests/accounting.rs builds an actual directory
tree — sparse file, hard link, symlink, unreadable directory — and asserts
each accounting rule this README claims. Those claims are the product; if one
of them silently breaks, the tool is confidently wrong, which is worse than
broken.
See CONTRIBUTING.md.
MIT OR Apache-2.0, at your option. See LICENSE-MIT and LICENSE-APACHE.
