A du -sh ~ on a developer Mac will sometimes flag the pnpm store as 60 GB and send you straight into a panic delete. The number is wrong. The pnpm store is content-addressable and hardlinked into every project's node_modules, so du counts the same inodes over and over. The honest size of the store itself is usually a fraction of that. This is the second-pass audit pain captured at brtkwr.com when one developer freed 200 GB on a full disk: "After the initial Docker/cache cleanup freed 150GB, going back and asking 'what else?' found another 75GB."
Where is the pnpm store on Mac?
The default pnpm store path on macOS changed over the last few releases. On a modern install (pnpm 8 and 9), the store lives at ~/Library/pnpm/store/v3. Older installs, or anyone who installed via Homebrew before pnpm switched its default, may have the store at ~/.local/share/pnpm/store/v3 or ~/.pnpm-store/v3. People who set PNPM_HOME or a corporate store-dir in .npmrc may have it anywhere.
There is exactly one command that tells the truth:
# Print the directory pnpm is using right now
pnpm store path
# And the disk size of that directory specifically
du -sh "$(pnpm store path)"
That second command is the only number you should trust. Anything else is a hardlink mirage. The store is one flat tree of content-hashed blobs, and pnpm rehydrates node_modules by hardlinking from this tree, which is why two projects sharing the same react@18.3.1 cost almost zero extra bytes after the first install.
Why does the pnpm store look so big?
Hardlinks. A file with one inode and ten hardlinks looks like ten files to most disk tools. du does not deduplicate inodes across separate directory trees, so the same package contents get counted in the store and in every project's node_modules.
The table below shows what this looks like on a Mac with five active pnpm projects sharing roughly the same dependency tree.
| Path | What du -sh reports |
What is actually new on disk |
|---|---|---|
~/Library/pnpm/store/v3 |
8.2 GB | 8.2 GB |
~/code/project-a/node_modules |
1.4 GB | ~50 MB (project-only deps) |
~/code/project-b/node_modules |
1.4 GB | ~50 MB |
~/code/project-c/node_modules |
1.4 GB | ~50 MB |
~/code/project-d/node_modules |
1.4 GB | ~50 MB |
~/code/project-e/node_modules |
1.4 GB | ~50 MB |
| Naive sum | 15.2 GB | 8.45 GB |
The naive sum is wrong by almost half. Finder's Get Info has the same problem. The honest audit asks: how big is the store directory itself, and how many of its blobs are still referenced? pnpm store prune answers the second half.
How do I run pnpm store prune safely?
pnpm store prune walks the store and deletes any package blob no longer referenced by a project pnpm knows about. It does not touch node_modules. It does not break a running pnpm dev. It does not invalidate lockfiles. The worst it can do is delete a blob for a package version you no longer have installed, which means your next pnpm install re-fetches it from the registry.
# Check store integrity first
pnpm store status
# Then prune unreferenced blobs
pnpm store prune
On a Mac that has cycled through twenty Node projects in a year, the prune typically reclaims 20 to 50 percent of the store. On a machine that mostly installs and reinstalls the same handful of projects, it reclaims almost nothing because every blob is still hardlinked into a live node_modules.
A six-step pnpm store cleanup playbook
This is the order I run when a Mac with three years of pnpm history hits a low-disk warning. The steps are read-only until step 5.
# 1. Find the real store path. Trust this, not your memory.
STORE="$(pnpm store path)"
echo "Store: $STORE"
du -sh "$STORE"
# 2. List every project on the machine with a pnpm-lock.yaml.
# These are the projects whose node_modules still hardlink the store.
find ~ -type f -name "pnpm-lock.yaml" \
-not -path "*/node_modules/*" \
-not -path "*/.Trash/*" 2>/dev/null
# 3. Sizes of every node_modules anywhere under home, sorted.
find ~ -type d -name node_modules \
-not -path "*/node_modules/*/node_modules*" \
-not -path "*/.Trash/*" 2>/dev/null \
| xargs -I{} du -sh {} 2>/dev/null | sort -h | tail -20
# 4. Verify store integrity before pruning.
pnpm store status
# 5. Prune unreferenced blobs from the global store.
pnpm store prune
# 6. Re-check store size, and the diff is your reclaim.
du -sh "$STORE"
Steps 1 through 4 do not touch a byte. Step 5 is the only mutation, and pnpm's prune logic is conservative: it errs on the side of keeping a blob if it cannot prove the blob is unreferenced. Step 6 is the receipt.
Project-local node_modules are the other half of this story. If you have a dozen abandoned side projects at ~/Experiments/, each one is still hardlinking into the store and pinning blobs in place. Deleting their node_modules is the prerequisite that lets the prune actually free anything. See how to find every node_modules folder on your Mac for the audit pattern.
Is it ever safe to delete the entire pnpm store directory?
Yes, with two caveats. First, every project that depended on the store will re-fetch its packages on the next pnpm install. On a fast connection that is ten minutes for a normal project and an hour for a monorepo. On a coffee-shop connection that is a workday. Second, if you have file: or link: dependencies, the next install may surface latent lockfile drift.
If you accept both, the deletion is one command:
# Quit any running dev servers first; pnpm holds file handles into
# the store via node_modules hardlinks while a process is alive.
mv "$(pnpm store path)" ~/.Trash/pnpm-store-$(date +%Y%m%d)
# Then on the next pnpm install, the store will be recreated.
Note the mv to Trash, not rm -rf. The Trash is the rollback window. If a critical project breaks on a Monday install, drag the store back out of Trash and avoid the bandwidth bill while you figure out which dependency to pin. The same Trash-first logic is why Move to Trash beats rm -rf for developer Mac cleanups.
How does pnpm's store cleanup compare to npm, Yarn, and Bun?
The four JavaScript package managers have very different storage models, which means the cleanup commands are not interchangeable.
| Tool | Default Mac path | Cleanup command | Hardlinks node_modules? |
Auto-cleanup |
|---|---|---|---|---|
| npm | ~/.npm/_cacache |
npm cache clean --force |
No | No |
| Yarn 1 | ~/Library/Caches/Yarn |
yarn cache clean |
No | No |
| Yarn Berry | .yarn/cache per project |
yarn cache clean --all |
No (uses zips) | No |
| pnpm | ~/Library/pnpm/store/v3 |
pnpm store prune |
Yes | No |
| Bun | ~/.bun/install/cache |
bun pm cache rm |
Optional | No |
The pnpm row is the unusual one. Because the store is load-bearing for installed projects, deleting it is more destructive than deleting an npm cache, even though both directories sit in your home folder and both can grow into the double-digit GB range. The npm cache is a pure download cache: removing it just means the next npm install re-fetches. The pnpm store is the actual package content for every pnpm project on the machine.
If you also run npm or Bun on the same Mac, those caches need their own audit. See where the npm cache lives on Mac and Bun cache location on Mac for the equivalent playbooks.
Why not just script a pnpm store prune cron?
You can. The reason it is not a complete answer is that pnpm store prune only reclaims blobs the store can prove are unreferenced. If stale node_modules directories sit in abandoned project folders, those references pin blobs in place no matter how often the cron runs. It will report "0 packages pruned" forever while the store keeps growing.
The honest scheduled job has two halves: a node_modules audit (delete the ones in projects you have not touched in 90 days) and a pnpm store prune (let pnpm reclaim what the audit just unpinned). The first half is the hard part to automate because it needs a judgement call about which projects are dead. That judgement is what CleanMyDev surfaces as a per-row receipt: project path, node_modules size, last-modified date, and a risk label so you tick the dead ones without nuking the live ones.
Closing: receipts before deletion, even for the pnpm store
The pnpm store on Mac is the rare cleanup target where the easy answer (rm -rf ~/Library/pnpm/store/v3) is also the wrong one. The right answer is an audit that names the unreferenced blobs and the abandoned project node_modules together, in one pass. If that audit belongs to you and not to a black-box cleaner that runs presets, then CleanMyDev is the $9.99 lifetime tool that gives you the receipt before it deletes anything. Per-row metadata, Move to Trash by default, no admin password, no subscription.
Related reading
Stop wondering what System Data is.
CleanMyDev opens the box. 110+ developer-specific cleanup targets. Move-to-Trash by default. $9.99 lifetime.