onlyfiles
v1.1.0
Published
Fast parallel disk-usage analyser with a local web UI — every folder and every file, biggest first. Show me what you're hiding down there.
Maintainers
Readme
You're at 400 GB used and you have no idea why. du -sh * tells you a folder is
big and then abandons you. Disk Utility shrugs and says "Other: 180 GB".
onlyfiles goes through absolutely everything and shows you the lot — every folder, every file, sorted biggest first, with a button next to each one. No subscription, no tipping. It was all sitting on your own hard drive the whole time.
npx onlyfilesThat's it. It scans your home directory, opens your browser, and waits for you to
start clicking. Nothing is uploaded anywhere: the server binds to 127.0.0.1 and
the only thing that ever leaves your machine is your dignity.
Getting it
Just run it. No install, always the latest:
npx onlyfiles # your home directory
npx onlyfiles ~/Library # somewhere specific
npx onlyfiles / --read-only # the whole disk, look-don't-touchOr keep it around, for when the urge strikes again:
npm install -g onlyfiles
onlyfilesNode 18+. One dependency. No build step. macOS, Linux and Windows.
The six tabs
Tree
Drill down folder by folder. The columns are name, size, share of parent, files and modified — that third one is the one to follow, because taking the biggest share at each level walks you straight to whatever is actually eating the disk.
Big folders load in pages (show N more), and any folder offers list every file here (live re-read) when you want its genuine file-by-file listing instead of the grouped one.
Reclaim candidates
Caches and build artefacts, grouped into categories with a running total, so a
hundred scattered node_modules become one line and one decision. Expand a
category to see and select the individual folders.
Every category says what it costs to get back, because that is not the same for all of them:
| Category | Why it's a candidate |
|---|---|
| node_modules | Reinstallable with npm/pnpm/yarn install |
| Build output | Regenerated by your build command |
| Bundler / tool caches | Regenerated on next run |
| AI model weights | Free to delete, but slow and bandwidth-heavy to download again |
| Python envs & caches | Recreatable from requirements/lockfile |
| Xcode | Xcode rebuilds these; DeviceSupport re-downloads |
| Package manager caches | Re-downloaded on demand |
| Containers / VMs | Docker & VM disk images — prune from the app |
| App caches | Apps regenerate these |
| Trash | Already deleted — emptying reclaims the space |
| iOS backups | Device backups; often several stale copies |
AI model weights is deliberately its own category instead of being folded in with the caches. Filing 58 GiB of model weights under "regenerated on next run" would be a lie, and a four-hour re-download is a nasty way to find that out.
Biggest files
The largest individual files anywhere in the scan, each with its full path. Where
you find the 28 GiB Docker.raw you forgot existed, and that ISO from 2023.
Fullest folders
Ranked by own bytes — what sits directly in each folder, not counting subfolders. Ranking by recursive total just lists your home directory's ancestors; this points at the folder genuinely holding the goods.
File types
Every byte grouped by extension, for the moment you learn that .rlib is 1.4 GiB
of your life.
Old & large
Big folders where nothing in the subtree has been modified recently — 6 months to 3 years, your pick. It's judged on the newest file anywhere inside, not the folder's own mtime, so a project full of current work doesn't get flagged merely because its top-level listing hasn't changed. Results are de-nested: one row per decision, rather than the same decision at eight different depths.
There's also a search box across every name in the scan, biggest matches first.
Doing something about it
Every row has four buttons:
| Button | What it does | |---|---| | Show | Highlights it in Finder / Explorer / your file manager, from the outside | | Open | Goes into the folder, or launches the file in whatever app handles it | | Copy | The full absolute path, ready to paste | | ✕ | Delete — see below, this bit isn't a joke |
Tick the checkboxes to select several and a bar appears with Show in Finder, Copy paths, Delete… and Clear. Selecting things never collapses or scrolls away anything you had open.
Ticking a folder takes everything inside it, and stays one entry rather than ten thousand. A folder with something ticked underneath it shows a dash instead of a tick, so a selection can never hide under a collapsed branch. Untick one thing inside a ticked folder and the selection is rewritten as "all of it except that" — put the thing back and it folds up into the folder again.
Rows can also be flagged no access (needs Full Disk Access), other volume (not descended into), or N links (a hardlinked file).
The delete part, stated plainly
Jokes off, because this is the part that can actually cost you something.
- You see the whole list first. The confirmation shows exactly what is going, nested the way it sits on disk, with a size against every line.
- Move to Trash is the default. Recoverable. The space comes back when you empty it.
- Delete permanently is a separate, explicit choice, and you have to type
DELETEto confirm it. --read-onlydisables deletion entirely. Show and Open still work.
Before anything is removed, the path must sit strictly inside the folder you
scanned, must not be the scan root, must not be a protected location, must survive
.. normalisation, and must actually exist. Protected locations match exactly,
so ~/Library/Caches stays deletable while ~/Library does not:
/ /System /Users /Applications /usr /bin /etc /var /private
$HOME ~/Library ~/Documents ~/Desktop ~/Downloads ~/Pictures
~/Movies ~/Music ~/.ssh ~/.gnupg ~/.configDon't run this under sudo. With sudo and a root of /, ordinary file
permissions stop being a backstop and the guards only protect exact paths. Use
--read-only for whole-disk scans.
The local API sits behind a random per-run token and a loopback-only Host check.
Without those, any web page open in your browser could POST to
localhost:4321/api/delete, which is a rude way to learn about CSRF.
Export
↓ Export offers Markdown report, CSV — every entry, Text tree and JSON. Or skip the interface entirely:
onlyfiles ~ --report md -o disk.md # summary, candidates, tree
onlyfiles ~ --report csv -o everything.csv # one row per entry, for a spreadsheet
onlyfiles ~ --report txt -o tree.txt # indented, du-like
onlyfiles ~ --report json -o tree.jsonReports stream, so a full home-directory CSV never has to fit in memory. Mine is 1.2 million rows and it doesn't care.
Changing what it looks at
📂 Change folder — or clicking the path in the header — opens Scan a different
folder. A browser can't hand a real filesystem path to a web page, so the picker
is driven by the server: navigate subfolders, type an absolute path, or use the
shortcuts for Home, Whole disk, Applications, Library,
Downloads, and any mounted volume under /Volumes.
show hidden folders is on by default, because .cache, .npm and .ollama
are usually the ones worth seeing. The same dialog sets the detail level, and
↻ Rescan re-runs the current folder from scratch.
Scanning / reports the whole Mac, not just the sealed 12 GiB system volume:
an APFS volume group presents one device id for both volumes, so the firmlinked
/Users is followed normally, while /System/Volumes stays on the skip list so
nothing gets counted twice.
External drives have to be their own root, since /Volumes is skipped while
scanning /:
onlyfiles /Volumes/BackupOptions
| Flag | What it does |
|---|---|
| -p, --port <n> | UI port (default 4321, walks upward if taken) |
| -m, --min-file-size <s> | Group files below this size per folder. 64k default, 0 itemises everything |
| --report <fmt> | md, csv, txt, json — write a report and exit |
| -o, --out <file> | Where --report writes (default stdout) |
| --depth <n> | Max tree depth in the report |
| -c, --concurrency <n> | Worker threads (default: CPUs − 2, capped at 8) |
| --cross-device | Also descend into other mounted volumes |
| --read-only | Disable deletion |
| --no-open | Don't launch a browser |
About --min-file-size
A home directory is mostly tiny files — a couple of million of them, nearly all
inside node_modules. Giving each one its own row costs a lot of memory and buys
nothing, because nobody reclaims 300 GiB by hunting 8 KiB files.
So by default, files under 64 KiB are folded into a single N smaller files row per folder. Byte totals are completely unaffected — every byte is still counted, just not itemised. And any folder's true listing is one click away via list every file here (live re-read).
Pass --min-file-size 0 to itemise everything up front.
The numbers are real
The unglamorous part, and the reason to trust everything above it.
- Size on disk, from
st_blocks × 512, so sparse files and APFS compression are reflected. Verified byte-exact againstdu -sk: 12,521,375,719 against 12,521,373,696 on a 12 GiB subtree — a 2 KB gap that isdu's own kilobyte rounding. - Hardlinks counted once. A file with five names occupies its blocks once. The
inode table is a lock-free set in a
SharedArrayBuffershared by every worker thread. With one table per worker, totals drifted by nearly a gigabyte between runs on a pnpm store, because the answer depended on which thread happened to see which copy first. - Symlinks are never followed, only counted as their own small entry.
- Volume boundaries aren't crossed unless you ask.
- Base-1024 with IEC labels (
GiB, notGB). Calling 1024³ a "GB" is how a tool ends up insisting your disk is 460 GB when Finder says 494 GB.
Speed
The walk is bound by lstat latency, not CPU, so it parallelises well. The main
thread walks the top few levels to build a frontier of subtree roots and a worker
pool drains it — which stops a 200k-file Library from queueing behind a 3-file
Documents.
Measured on a 12-core M-series Mac, home directory, 2,664,639 entries / 301 GiB:
| | Time | | |---|---|---| | 8 workers, warm cache | 19.1 s | ~1.0 GB peak RSS | | 1 worker, warm cache | 64.8 s | 3.4× slower | | single-threaded, cold cache | ~5 min | that's the disk, not the code |
Both worker counts report byte-identical totals. That is the entire point of the shared inode table.
Permissions
On macOS some folders are sealed by TCC and can't be read without Full Disk
Access. onlyfiles flags those rows no access rather than quietly counting
them as empty, and shows the count in the header — around 134 in a typical home
directory, 439 when scanning /.
To include them: System Settings → Privacy & Security → Full Disk Access, then add your terminal.
Licence
MIT. Go on then.
