Verify latest release
pnpm version
11.5.2
Which area(s) of pnpm are affected? (leave empty if unsure)
Store
Link to the code that reproduces this issue or a replay of the bug
https://github.com/rubnogueira/pnpm-repro-sharedlockfile
Reproduction steps
.
├── package.json # { "name": "repro-root", "private": true }
├── pnpm-workspace.yaml
└── libs/common
├── package.json # { "name": "@repro/common", "dependencies": { "is-odd": "3.0.1" } }
└── pnpm-workspace.yaml
pnpm-workspace.yaml (root):
packages:
- libs/*
enableGlobalVirtualStore: true
sharedWorkspaceLockfile: false
libs/common/pnpm-workspace.yaml (makes the package its own workspace root, with its own lockfile):
enableGlobalVirtualStore: true
Steps:
rm -rf node_modules libs/common/node_modules pnpm-lock.yaml libs/common/pnpm-lock.yaml
pnpm install # at root — OK
cd libs/common && pnpm install # prompts to remove & recreate node_modules
Describe the Bug
Installation / global virtual store (enableGlobalVirtualStore), post-install build step.
When enableGlobalVirtualStore: true is set, running pnpm install at the workspace root succeeds, but a subsequent pnpm install inside a workspace package prompts:
? The modules directory at ".../libs/common/node_modules" will be removed and
reinstalled from scratch. Proceed? (Y/n)
(In a non-TTY/CI shell the same condition surfaces as ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY.) The prompt comes back every time you install in that package, even though nothing changed.
Expected Behavior
The second pnpm install is a no-op ("Already up to date") — node_modules is already valid and should not be purged.
Actual behavior
pnpm reports a virtual-store mismatch and wants to recreate node_modules:
[ERR_PNPM_UNEXPECTED_VIRTUAL_STORE] Unexpected virtual store location
The dependencies at ".../libs/common/node_modules" are currently symlinked from the virtual store directory at ".../libs/common/node_modules/.pnpm". pnpm now wants to use the virtual store at ".../store/v11/links" ...
Root cause
During a workspace install with enableGlobalVirtualStore, each project's node_modules/.modules.yaml is written twice, with conflicting virtualStoreDir values:
- The install/link step writes the correct GVS value virtualStoreDir = /links — extendInstallOptions (installing/deps-installer/src/install/extendInstallOptions.ts) sets virtualStoreDir to /links when GVS is enabled.
- The post-install build pass — buildProjects (building/after-install/src/index.ts), run as the per-project rebuild during a workspace install — calls getContext() and rewrites .modules.yaml. Its options come from extendBuildOptions (building/after-install/src/extendBuildOptions.ts), which — unlike extendInstallOptions — never sets virtualStoreDir for GVS. So getContext falls back to the per-project default node_modules/.pnpm and the rewrite overwrites the correct value the install step recorded.
The next install in that project recomputes /links, compares it to the recorded node_modules/.pnpm, fails checkCompatibility with ERR_PNPM_UNEXPECTED_VIRTUAL_STORE, and — because a plain install runs with forceNewModules — prompts to purge node_modules.
The root workspace usually escapes this only because the optimistic "Already up to date" check short-circuits before validation (and a depless root manifest passes the deps-status check), so the mismatch surfaces in packages that actually have dependencies.
Which Node.js version are you using?
24.16.0
Which operating systems have you used?
If your OS is a Linux based, which one it is? (Include the version if relevant)
No response
Verify latest release
pnpm version
11.5.2
Which area(s) of pnpm are affected? (leave empty if unsure)
Store
Link to the code that reproduces this issue or a replay of the bug
https://github.com/rubnogueira/pnpm-repro-sharedlockfile
Reproduction steps
.
├── package.json # { "name": "repro-root", "private": true }
├── pnpm-workspace.yaml
└── libs/common
├── package.json # { "name": "@repro/common", "dependencies": { "is-odd": "3.0.1" } }
└── pnpm-workspace.yaml
pnpm-workspace.yaml(root):libs/common/pnpm-workspace.yaml (makes the package its own workspace root, with its own lockfile):
Steps:
rm -rf node_modules libs/common/node_modules pnpm-lock.yaml libs/common/pnpm-lock.yaml
pnpm install # at root — OK
cd libs/common && pnpm install # prompts to remove & recreate node_modules
Describe the Bug
Installation / global virtual store (
enableGlobalVirtualStore), post-install build step.When
enableGlobalVirtualStore: trueis set, runningpnpm installat the workspace root succeeds, but a subsequentpnpm installinside a workspace package prompts:(In a non-TTY/CI shell the same condition surfaces as
ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY.) The prompt comes back every time you install in that package, even though nothing changed.Expected Behavior
The second pnpm install is a no-op ("Already up to date") — node_modules is already valid and should not be purged.
Actual behavior
pnpm reports a virtual-store mismatch and wants to recreate node_modules:
[ERR_PNPM_UNEXPECTED_VIRTUAL_STORE] Unexpected virtual store location
The dependencies at ".../libs/common/node_modules" are currently symlinked from the virtual store directory at ".../libs/common/node_modules/.pnpm". pnpm now wants to use the virtual store at ".../store/v11/links" ...
Root cause
During a workspace install with enableGlobalVirtualStore, each project's node_modules/.modules.yaml is written twice, with conflicting virtualStoreDir values:
The next install in that project recomputes /links, compares it to the recorded node_modules/.pnpm, fails checkCompatibility with ERR_PNPM_UNEXPECTED_VIRTUAL_STORE, and — because a plain install runs with forceNewModules — prompts to purge node_modules.
The root workspace usually escapes this only because the optimistic "Already up to date" check short-circuits before validation (and a depless root manifest passes the deps-status check), so the mismatch surfaces in packages that actually have dependencies.
Which Node.js version are you using?
24.16.0
Which operating systems have you used?
If your OS is a Linux based, which one it is? (Include the version if relevant)
No response