Subchapter 6.6
references/core-store.mdMarkdown5 KBView on GitHub
pnpm uses a content-addressable store to save disk space and speed up installations. All packages are stored once globally and hard-linked to project node_modules.
<store-dir>/ # Global content-addressable store (pnpm store path)
└── files/
└── <hash>/ # Files stored by content hash
project/
└── node_modules/
├── .pnpm/ # Virtual store (hard links to global store)
│ ├── lodash@4.17.21/
│ │ └── node_modules/
│ │ └── lodash/
│ └── express@4.18.2/
│ └── node_modules/
│ ├── express/
│ └── <deps>/ # Flat structure for dependencies
├── lodash -> .pnpm/lodash@4.17.21/node_modules/lodash
└── express -> .pnpm/express@4.18.2/node_modules/express# Show store location
pnpm store path
# Remove unreferenced packages
pnpm store prune
# Check store integrity
pnpm store status
# Add package to store without installing
pnpm store add <pkg>Store/linker settings live in pnpm-workspace.yaml (camelCase), not .npmrc.
storeDir: ~/.local/share/pnpm/storeThe default store path is OS-specific (e.g. ~/.local/share/pnpm/store on Linux, ~/Library/pnpm/store on macOS). Find it with pnpm store path.
The virtual store (.pnpm in node_modules) contains hard links to the global store:
virtualStoreDir: node_modules/.pnpm
virtualStoreDirMaxLength: 60 # lower this for long-path issues on Windows
nodeLinker: hoisted # alternative flat layoutpnpm saves significant disk space:
# Compare actual vs apparent size
du -sh node_modules # Apparent size
du -sh --apparent-size node_modules # With hard links countedWith enableGlobalVirtualStore: true, projects skip the per-project node_modules/.pnpm directory entirely; their node_modules contains only symlinks into one shared virtual store at <store-path>/links/, keyed by dependency-graph hash. In pnpm v11 it is the default for pnpm dlx/pnx and global installs; for project installs it is still opt-in. See features-global-virtual-store for details and the git-worktrees multi-agent workflow.
enableGlobalVirtualStore: trueConfigure how node_modules is structured (nodeLinker in pnpm-workspace.yaml):
nodeLinker: isolated # default: symlinked virtual store (strict, no phantom deps)
# nodeLinker: hoisted # flat node_modules (npm-like) for tools that dislike symlinks
# nodeLinker: pnp # Plug'n'Play, no node_modules (set `symlink: false` too)node_modules like npmCache build outputs for native modules (enabled by default):
sideEffectsCache: true
sideEffectsCacheReadonly: false # only read the cache, don't create itfrozenStore: true (v11.7+) lets pnpm install run against a read-only store (Nix store, read-only bind mount, OCI layer). Pair with --offline --frozen-lockfile; the store must already contain everything, including approved build outputs.
pnpm install --frozen-store --offline --frozen-lockfileFor CI/CD, you can share the store:
# GitHub Actions example
- uses: pnpm/action-setup@v4
with:
run_install: false
- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}# Verify and fix store
pnpm store status
pnpm store prune# auto (default) tries clone -> hardlink -> copy
packageImportMethod: copy# Fix store permissions (find the path with `pnpm store path`)
chmod -R u+w "$(pnpm store path)"