---
title: 'Where did the disk space go? Auditing caches on a small VPS'
url: 'https://docs.agenteum.top/blog/2026-10-17-vps-disk-audit'
markdown: 'https://docs.agenteum.top/blog/2026-10-17-vps-disk-audit.md'
date: '2026-10-17'
description: 'A practical disk audit on a small VPS: mapping du output, separating disposable caches from load-bearing directories, and reclaiming 10 GB without breaking anything. Includes a Hermes Agent cleanup section.'
taxonomy:
  category:
    - tech
  tag:
    - linux
    - vps
    - server
    - maintenance
    - disk
    - cleanup
    - hermes
  archives_month:
    - oct_2026
  archives_year:
    - '2026'
---

# Where did the disk space go? Auditing caches on a small VPS

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:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
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](https://hermes-agent.nousresearch.com), 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:

```bash
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
- `~/.cache` is disposable by definition; `~/.local` is 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.

---

## Navigation

- Parent: [Blog](https://docs.agenteum.top/blog.md)
- Next: [Samsung Galaxy S24 FE stuck after One UI 9 update with AccessDenied error](https://docs.agenteum.top/blog/2026-10-10-galaxy-s24-fe-one-ui-9-access-denied-wizard.md)
