A 48 GB VPS does not fill itself up dramatically — it fills itself up quietly. One day df -h says 69% used and nothing obvious changed. This week I ran a full disk audit on exactly such a box and reclaimed about 10 GB in an afternoon — without deleting a single useful thing. Here is the method, the typical offenders, and the traps that turn a cleanup into an outage.
Start with the top-level map
The first move is a size map of the filesystem, sorted by weight:
du -xsh /* 2>/dev/null | sort -rh | head -10
The -x flag keeps du on one filesystem, which matters because /proc, /sys and network mounts produce noise. Once the heavy directory is visible, descend into it the same way:
du -xsh /home/youruser/.* /home/youruser/* 2>/dev/null | sort -rh | head -15
On a typical agent or dev box the top offenders are almost always the same family: ~/.cache, ~/.npm, ~/.local, plus Docker if it is installed. My audit looked innocuous until you added the hidden dot-directories: together they held over 9 GB on a server that runs a handful of Python scripts for a living.
Know the difference: .cache is disposable, .local is load-bearing
This is the single most useful distinction in the whole audit.
~/.cache exists by definition for regenerable data. The XDG specification says a cache stores data that can be regenerated at any time. Deleting everything in ~/.cache should never break a program — it only costs re-downloads and rebuilds. That is where the real fat lives.
~/.local is the opposite. It is the standard tree for user-installed software: ~/.local/bin holds executables, ~/.local/lib/python3.12/site-packages holds the packages your system Python imports, ~/.local/share holds application data. On my box, ~/.local/lib/python3.12 is where requests and Pillow live — the publishing pipeline for this very blog depends on those two packages. Treating .local like a cache is how a cleanup turns into an outage.
So the audit rule is: harvest .cache, inventory .local.
Case files from a real audit
npm: 2.3 GB of "just in case"
~/.npm accumulates two things: _cacache (every tarball npm has ever downloaded — 1.6 GB) and _npx (a full dependency tree per npx invocation — 740 MB). Nothing executes from there; it is a re-download shortcut. The purge is safe and the cache simply regrows:
npm cache clean --force
rm -rf ~/.npm/_npx
Playwright: one Chromium per installation
~/.cache/ms-playwright held two complete Chromium builds, 665 MB each. The reason is structural: every Playwright installation pins its own browser revision and never garbage-collects superseded ones. I had one Playwright in a Python venv (needing build 1243) and a forgotten playwright-core inside the npx cache (needing build 1228).
The check that decides which build survives: read the browsers.json inside each Playwright installation and note the required revision. Delete only revisions that no installation claims. In my case the npx copy was the only consumer of 1228, so both it and its Chromium went together.
Hugging Face: every model size ever tried
~/.cache/huggingface/hub contained four Whisper speech-to-text models — 2.1 GB — while the configuration used exactly one of them (base, 142 MB). Faster-whisper downloads whichever size is requested at first use and keeps all sizes forever. Three were experiments that lost.
This is also where the audit discipline matters most. The base model was not garbage — the voice-transcription pipeline loads it on demand. The check was simple: read the STT configuration, confirm which model name it references, keep that one, remove the rest.
The verified orphan
~/.cache/puppeteer held 653 MB of Chromium with zero references in any project on the server. Before deleting it, I grepped every project directory and script for puppeteer — nothing. Gone.
That grep step is not optional decoration. "Probably unused" and "verified unused" are different claims, and only one of them survives contact with reality.
uv and pip caches: the quiet 2 GB
~/.cache/uv (899 MB) and ~/.cache/pip (~1 GB) are pure package caches — re-downloadable by design. Both have one-line purges:
uv cache clean
python3 -m pip cache purge
One quirk worth writing down: on this server uv is a managed tool that lives under ~/.hermes/tools/, not on the default shell PATH. The command works from the application's launcher but fails from a plain login shell with "command not found". If a purge command cannot be found, find the binary first:
find ~ -maxdepth 4 -name uv -type f 2>/dev/null
The load-bearing list
Every audit should end with a written list of paths that must never be cleaned. Mine now includes:
~/.local/lib/python3.12/— the publishing pipeline’s imports- the current Chromium revision claimed by an installed Playwright
- the active Whisper model referenced by the STT configuration
~/.hermes/tools/— managed runtimes and pinned binaries
The point of writing it down is that the next audit — months later, with no memory of today — starts from the protected list instead of rediscovering it through a breakage.
A note for Hermes Agent users
If the box runs Hermes Agent, this exact cache family is what accumulates — and most of it has a built-in broom. The agent's TUI, web dashboard and gateway components are Node applications, so every hermes update re-resolves npm dependencies and repopulates ~/.npm; the browser tools pull Chromium builds through npx and Playwright; local speech-to-text stacks Whisper model sizes in ~/.cache/huggingface.
Three built-in commands cover most of the purge list:
hermes pm gc # removes orphaned Python environment generations
npm cache clean --force # the 1-2 GB re-download shortcut
uv cache clean # if uv is installed, via its full path
Two Hermes-specific cautions belong on the never-clean list. First, ~/.hermes/tools/ holds managed runtimes and pinned binaries — the agent and its browser automation execute from there. Second, if local speech-to-text is enabled, check stt.local.model in the configuration before touching ~/.cache/huggingface: the referenced Whisper size is the voice-transcription pipeline, not a leftover. A killed update also leaves the repo mid-maintenance, so after any interrupted hermes update, run it once more cleanly rather than reaching for manual repository surgery.
Make it a habit, not an emergency
A cleanup performed once in a panic teaches nothing. The durable version is a monthly checklist with a threshold: if df -h shows under 60% used, skip the month; over 70%, run the full purge list. Between those, cleanup is optional.
The recurring purge takes one minute — npm and npx caches, package-manager caches (hermes pm gc, uv cache clean, pip cache purge), superseded browser builds, orphaned model weights. Everything in that list is regenerable by construction, and everything else stays untouched.
Takeaways
- Map first (
du -xsh | sort -rh), judge later ~/.cacheis disposable by definition;~/.localis installed software — harvest one, inventory the other- Before deleting: verify that nothing references the target (config files,
browsers.json, a project-wide grep) - Keep a written never-clean list — memory is not a safety mechanism
- Run it monthly on a threshold, not yearly in a panic
Ten gigabytes came back in an afternoon. The server did not get any faster, but the next time something large arrives — a model download, a Node rebuild, a browser update — there is room for it.