pnpm’s Content‑Addressable Store: How Hard Links Save Disk Space and Catch Phantom Dependencies
pnpm’s content‑addressable store uses hard links to share dependency files across projects, saving disk space and surfacing phantom dependencies early. This guide explains the mechanism, shows a concrete example, and outlines trade‑offs for real‑world use.
05 Feb 2026, 20:32 UTC

Problem: Duplicate Packages and Hidden Bugs
In a typical JavaScript project, every npm install copies all dependency files into node_modules. When you have multiple projects or a monorepo, the same library can be stored dozens of times, wasting disk space. Worse, the flat node_modules layout used by npm and Yarn can hide bugs where code imports a transitive dependency that has been hoisted but was never declared in the project’s package.json. These “phantom” dependencies surface only at runtime, making debugging painful.
How pnpm’s Store Works
pnpm solves both problems with a two‑step approach:
- Global content‑addressable store – Every package file is stored once in a shared directory (default
~/.local/share/pnpm/storeon Linux/macOS,%APPDATA%\pnpm\storeon Windows). The store key is a hash of the package contents, so identical files are deduplicated across all projects. - Hard links into
node_modules– When a project installs a dependency, pnpm creates hard links from the global store into the project’snode_modules/.pnpmdirectory. Hard links share the same inode, so the file on disk exists only once, even if multiple projects point to it.
This is why you often see a single lodash folder in the store, even if you run pnpm add lodash in ten different projects.
Benefits: Disk Savings and Phantom‑Dependency Safety
Disk space – Because hard links point to the same underlying file, installing the same version of a library in many projects costs almost nothing extra. On an SSD, the difference is negligible, but on a traditional HDD or when you have a large monorepo, the savings can be several gigabytes.
Phantom‑dependency protection – pnpm’s non‑flat layout means a package’s node_modules contains only its declared dependencies. Any attempt to import an undeclared transitive module fails at install time or throws a clear error at runtime. This surface‑level check prevents subtle bugs that arise when a library accidentally relies on a sibling package that happens to be hoisted by npm/Yarn.
Practical Setup Considerations
- All projects that rely on the same store must live on the same filesystem. If you clone a repo to a different drive, pnpm will error or fall back to a slower copy‑on‑write mode.
- Configure the store path with
pnpm store pathor setstore-dirin.npmrcif you need a custom location. - Workspaces are enabled via
pnpm-workspace.yaml. Usepnpm --filter <pkg> run buildto target a specific package. - Some tools expect a flat
node_modules. If you must support them, setnode-linker=hoistedin.npmrc, but be aware this disables phantom‑dependency checks. - Packages with
installorpostinstallscripts may run differently because pnpm historically restricts build scripts. Verify the behavior in the version you use.
Worked Example: Installing Lodash Twice
Assume you have two projects, proj-a and proj-b, each with its own package.json that declares lodash@^4.17.21. Follow these steps on a single Linux machine.
# 1. Verify pnpm version
pnpm --version
# 2. Create two projects
mkdir proj-a proj-b
cd proj-a
pnpm init -y
pnpm add lodash@^4.17.21
cd ../proj-b
pnpm init -y
pnpm add lodash@^4.17.21
# 3. Inspect the store
pnpm store path
# (e.g., /home/user/.local/share/pnpm/store)
# 4. Compare disk usage
du -sh proj-a/node_modules/.pnpm proj-b/node_modules/.pnpm
# Both should reference the same files via hard links.
# 5. Verify hard link count
find proj-a/node_modules/.pnpm -maxdepth 1 -type l -ls | grep lodash
# The output should show a link count of 2 for the same inode.
To check phantom‑dependency safety, try importing a non‑declared dependency:
# In proj-a, create src/index.js
mkdir src
cat > src/index.js <<EOF
const fn = require('lodash/merge');
console.log(fn({a:1}, {b:2}));
EOF
# Run the file
node src/index.js
# It will succeed because lodash is declared.
# Now attempt to import a transitive dependency that is not declared.
cat > src/other.js <<EOF
const foo = require('chalk'); // chalk is not declared
console.log(foo('Hello'));
EOF
node src/other.js
# pnpm will throw an error: "Cannot find module 'chalk'".
Notice that npm/Yarn would silently hoist chalk if it were a dependency of lodash, potentially masking the problem.
Trade‑Offs and Limitations
- Filesystem requirement – Hard links only work within a single filesystem. If your CI agents or developers clone projects onto separate drives, pnpm will either error or copy files, negating disk‑space benefits.
- Tool compatibility – Some build tools, linters, or IDEs assume a flat
node_modules. They may fail or misbehave with pnpm’s symlinked layout unless you enablenode-linker=hoisted. - Build scripts – pnpm’s default behavior in newer major versions is to reject arbitrary
installorpostinstallscripts for security. If a package relies on such scripts, you must explicitly allow them with--ignore-scriptsor adjust the package’spnpmfile.js. - Version‑specific defaults – The store location, build‑script approval, and other defaults change between pnpm releases. Always check the official docs for the exact version you are using.
Actionable Takeaway
If you’re working on a multi‑project or monorepo JavaScript/TypeScript codebase, consider switching to pnpm to:
- Deduplicate dependency files and shrink disk usage.
- Catch phantom dependencies before they become runtime bugs.
- Leverage the built‑in workspace support for cleaner monorepo workflows.
Start by running pnpm install in an existing project and inspect node_modules/.pnpm to confirm hard links. If you run into tool compatibility issues, experiment with node-linker=hoisted as a temporary workaround while you update your tooling to support pnpm’s layout.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.