Summary
When the only change since the last install is to pnpm-lock.yaml (e.g., git checkout, git reset, or any external modification), pnpm install short-circuits with Already up to date and does not re-validate the lockfile against the manifests. This is a regression introduced in pnpm v11 by optimisticRepeatInstall (default-on).
Reproduction
In a workspace (single project also affected):
# Clean state
rm -rf node_modules pnpm-lock.yaml
pnpm install # installs everything, creates node_modules/.pnpm-workspace-state-v1.json
# Now roll back only the lockfile — manifests stay put
git checkout HEAD~1 -- pnpm-lock.yaml
# (or equivalently: touch pnpm-lock.yaml, or any external lockfile edit)
pnpm install
# => "Already up to date" ← BUG: lockfile was just changed, pnpm did not re-validate
Verified on pnpm v11.5.0 (published release) in ~/fathom/frontend/fathom-frontend-bench1 (27-project Angular monorepo, lockfileVersion 9.0).
Control test (correct behavior)
touch package.json
pnpm install
# => full re-install (manifest mtime triggers re-resolution)
So the optimistic check is correctly triggered by manifest changes, but not by lockfile-only changes.
Root cause
deps/status/src/checkDepsStatus.ts:263-271:
const modifiedProjects = allManifestStats.filter(
({ manifestStats }) =>
manifestStats.mtime.valueOf() > workspaceState.lastValidatedTimestamp
)
if (modifiedProjects.length === 0) {
logger.debug({ msg: 'No manifest files were modified since the last validation. Exiting check.' })
return { upToDate: true, workspaceState }
}
When the lockfile is changed (e.g., via git checkout), its mtime is updated, but no manifest's mtime is touched. So modifiedProjects is empty, the function returns upToDate: true at line 270, and the lockfile content is never validated against the current manifests.
The lockfile-vs-current-lockfile equality check at line 292 only runs inside the modifiedProjects loop and never fires when no manifest was modified.
Why this is a regression from v10
In v10, optimisticRepeatInstall defaulted to false (see #11158). The same git checkout flow in v10 would trigger a normal install that re-validates the lockfile.
Impact
Affects any workflow where the lockfile can change without a corresponding manifest change:
git checkout / git reset / git stash pop of the lockfile
- External tools that edit the lockfile (Renovate, Dependabot, custom scripts)
- Manual lockfile edits (e.g., to fix a merge conflict)
- Branch switches where the lockfile differs
In all of these, pnpm will silently keep a stale lockfile that may not satisfy the current manifests, leading to subtle bugs (e.g., wrong versions installed, missing transitive deps).
Workaround
pnpm install --config.optimistic-repeat-install=false
Or, temporarily:
touch package.json && pnpm install
Suggested fix
In the modifiedProjects.length === 0 branch, also check the wanted lockfile's mtime vs lastValidatedTimestamp. If the lockfile is newer, don't short-circuit — fall through to lockfile-content validation (which is already implemented at line 292+ and is fast since it only reads files, no network).
A more robust fix would be to store a hash of the lockfile content (or its mtime) in the workspace state and compare on the next run, so any lockfile change is detected regardless of how it happened.
Related issues
Written by an agent (opencode, minimax-m3-free).
Summary
When the only change since the last install is to
pnpm-lock.yaml(e.g.,git checkout,git reset, or any external modification),pnpm installshort-circuits withAlready up to dateand does not re-validate the lockfile against the manifests. This is a regression introduced in pnpm v11 byoptimisticRepeatInstall(default-on).Reproduction
In a workspace (single project also affected):
Verified on pnpm
v11.5.0(published release) in~/fathom/frontend/fathom-frontend-bench1(27-project Angular monorepo, lockfileVersion 9.0).Control test (correct behavior)
touch package.json pnpm install # => full re-install (manifest mtime triggers re-resolution)So the optimistic check is correctly triggered by manifest changes, but not by lockfile-only changes.
Root cause
deps/status/src/checkDepsStatus.ts:263-271:When the lockfile is changed (e.g., via
git checkout), its mtime is updated, but no manifest's mtime is touched. SomodifiedProjectsis empty, the function returnsupToDate: trueat line 270, and the lockfile content is never validated against the current manifests.The lockfile-vs-current-lockfile equality check at line 292 only runs inside the
modifiedProjectsloop and never fires when no manifest was modified.Why this is a regression from v10
In v10,
optimisticRepeatInstalldefaulted tofalse(see #11158). The samegit checkoutflow in v10 would trigger a normal install that re-validates the lockfile.Impact
Affects any workflow where the lockfile can change without a corresponding manifest change:
git checkout/git reset/git stash popof the lockfileIn all of these, pnpm will silently keep a stale lockfile that may not satisfy the current manifests, leading to subtle bugs (e.g., wrong versions installed, missing transitive deps).
Workaround
Or, temporarily:
touch package.json && pnpm installSuggested fix
In the
modifiedProjects.length === 0branch, also check the wanted lockfile's mtime vslastValidatedTimestamp. If the lockfile is newer, don't short-circuit — fall through to lockfile-content validation (which is already implemented at line 292+ and is fast since it only reads files, no network).A more robust fix would be to store a hash of the lockfile content (or its mtime) in the workspace state and compare on the next run, so any lockfile change is detected regardless of how it happened.
Related issues
optimisticRepeatInstallcausespnpm installto no-op for most changes inpnpm-workspace.yaml#10393:optimisticRepeatInstallno-ops forpnpm-workspace.yamlchanges (fixed by fix: detect overrides and other lockfile-affecting setting changes in optimisticRepeatInstall #10654, unreleased). Different aspect of the same feature.supportedArchitectureschanges not detected.file:directory dependency changes not detected (different root cause).Written by an agent (opencode, minimax-m3-free).