What Changed in pnpm Since 11.1.2 Until 11.7.0
Six pnpm releases shipped while my project was frozen at 11.1.2. Two of them added security controls I should have adopted sooner.
Six pnpm releases shipped between 11.1.2 and 11.7.0. The 11.3 and 11.4 releases added supply-chain security controls or patched vulnerabilities I should have adopted sooner. I’d also been sitting on 11.1’s audit signatures since May. I pinned my project to 11.1.2 and didn’t look up until now. The security additions were the headline; two migration behaviors that weren’t in the changelog ate most of the actual time.
Upgrade baseline
My pnpm-workspace.yaml had comprehensive supply chain controls: minimumReleaseAge, trustPolicy: no-downgrade, blockExoticSubdeps, allowBuilds, strictDepBuilds, and the rest. But three things were missing.
First, my CI ran pnpm audit --prod and pnpm audit --audit-level high, but neither verified that the packages on disk matched what the registry signed. The advisory database says whether a package version has known CVEs. It does not say whether the tarball you downloaded is the one the maintainer published. ECDSA registry signatures fill that gap, and pnpm 11.1 added a command for it.
Second, strictPeerDependencies: true catches peer dependency issues during pnpm install, but my CI uses preferFrozenLockfile which skips resolution when the lockfile is satisfied. A dependency upgrade that introduces a peer conflict passes pnpm install silently if the lockfile was generated by a different machine. I had no way to scan the existing lockfile for peer issues.
Third, minimumReleaseAge and trustPolicy re-verify every package on every pnpm install, even when the lockfile hasn’t changed and was already verified on the machine that generated it. In CI with preferFrozenLockfile, this re-verification is redundant. It is also the most memory-intensive part of the install.
Beyond feature gaps, 11.4 patched five supply-chain vulnerability classes I hadn’t tracked: lockfile integrity validation, credential scoping across registries, git resolution hardening, patch file integrity, and dependency alias attacks.
Release-by-release changes
pnpm 11.0: Settings moved out of package.json
This was the change that bit me first. Since v11, pnpm no longer reads the pnpm field in package.json. The settings that used to live there (overrides, peerDependencyRules, packageExtensions, and the rest) all moved to pnpm-workspace.yaml. This is part of the v10 to v11 migration, and there is an official codemod for it: pnpx codemod run pnpm-v10-to-v11.
The deprecation prints a warning and keeps going:
[WARN] The "pnpm" field in package.json is no longer read by pnpm.
The following keys were ignored: "pnpm.overrides".
In a long install log that warning is easy to miss, and the install still completes. The override silently has no effect, and the lockfile does not change. I cover what that cost me in Two behaviors that bit me during the migration.
pnpm 11.1: pnpm audit signatures
The pnpm audit signatures command verifies that every installed package has a valid ECDSA signature from the registry’s public key. Each registry publishes its signing keys at /-/npm/v1/keys. pnpm fetches them, checks the signature on each package’s integrity record, and exits with code 1 if anything fails.
This is different from pnpm audit (which checks against the npm advisory database for known CVEs) and trustPolicy (which verifies provenance attestations haven’t been downgraded). Signatures verify the artifact; audits verify the version; trust policy verifies the supply chain hasn’t regressed.
I added it to the Dagger CI pipeline right after the advisory audits:
// ci/src/index.ts — standalone @func()
@func()
async auditSignatures(source: Directory): Promise<string> {
return this.nodeBase(source)
.withExec(["pnpm", "audit", "signatures"])
.stdout();
}
// In the chained check() pipeline, it runs third:
const steps = [
{ name: "audit-prod", cmd: ["pnpm", "audit", ...] },
{ name: "audit-all", cmd: ["pnpm", "audit", ...] },
{ name: "audit-signatures", cmd: ["pnpm", "audit", "signatures"] },
// ...
];
The command respects scoped registries configured in pnpm-workspace.yaml. Registries that don’t publish signing keys are skipped. With --json, the output is machine-readable.
pnpm 11.1 also added:
pnpm bugsandpnpm owner: open a package’s bug tracker or list its owners from the CLI- Install from arbitrary named registries, including a built-in alias for GitHub Packages npm registry
--no-runtimeto skipdevEngines.runtimeinstallation in CI
pnpm 11.3: trustLockfile and native commands
The trustLockfile setting skips the supply-chain verification pass that re-applies minimumReleaseAge and trustPolicy to lockfile entries on every install. When enabled, pnpm treats the lockfile as already verified.
I enabled it in pnpm-workspace.yaml:
# pnpm-workspace.yaml
trustLockfile: true
The tradeoff: a poisoned lockfile committed by a compromised collaborator would bypass verification. In my case, this project has no outside collaborators and every commit passes CI with pnpm audit signatures and advisory audits. The lockfile is reviewed in PRs. The risk is minimal and the CI speedup is real. The docs call this out explicitly: it is for “closed-source projects with trusted authors.”
11.3 also shipped native TypeScript implementations of pnpm pkg, pnpm repo, and pnpm set-script, replacing the npm CLI fallback. These are minor quality improvements; they don’t change behavior but remove a dependency on npm being installed.
Two other 11.3 additions I noted but didn’t adopt:
pnpm stage: staged publishing for npm packages (I don’t publish packages from this project)--skip-manifest-obfuscationforpnpm packandpnpm publish(same reason)
pnpm 11.4: Supply-chain patches
This was the most impactful release for security. It closed five vulnerability classes:
Tarball-integrity mismatches are now a hard failure. If a package tarball’s integrity hash doesn’t match the lockfile, pnpm install fails instead of warning. You can opt in to refreshing integrity values from the registry with --update-checksums, but --force and pnpm update don’t bypass the check. This prevents a corrupted or tampered tarball from being silently accepted.
Unscoped credentials no longer leak across registries. A credential configured for one registry (//registry.npmjs.org/:_authToken) was previously usable by other registries in some configurations. This is now blocked.
Lockfile entries without integrity are rejected. A missing integrity field on a lockfile entry is treated as a validation error, not silently accepted.
Git resolutions reject non-SHA commit fields. Tags and branch names in resolution.commit are rejected. Only full SHA-1 hashes pass validation.
Patch file and dependency alias hardening. The exact details are in the release notes, but the summary is that crafted patch files and dependency aliases can no longer redirect package resolution.
Each of these is a defense against a known attack class. None of them required configuration changes on my side; the protections apply by default.
What I already had
Some features shipped in pnpm 11.0 and 11.1 that were already in my config from the start:
-
pnpm peers check(11.0) analyzes the lockfile for unmet and missing peer dependencies. I added it to CI as a standalone step:// ci/src/index.ts { name: "peers-check", cmd: ["pnpm", "peers", "check"] },This catches peer dependency regressions between
preferFrozenLockfileinstalls. -
devEngines.packageManager(11.0) is the newer way to declare pnpm version ranges. I started this migration on thepackageManagerfield, then removed it oncemise.tomlbecame the single source of truth (see Verification). -
pnpm createnow honors project security policies (minimumReleaseAge,trustPolicy) when fetching starter kits.
What I skipped
Not every new feature applies to a static blog:
devEngines.runtime: automatically downloads Node.js. I use mise for that; adding a second runtime manager would create conflicts.pnpm stage: staged publishing. No packages to publish.pnpm version: bump package versions. This project has a single root package with no versioning needs.- Install from arbitrary registries: I use the public npm registry exclusively.
Two behaviors that bit me during the migration
The version bump itself was mechanical: the same 11.1.2 to 11.7.0 replacement across CI workflows, Dagger modules, mise.toml, and docs. Two behaviors ate the rest of the time, and neither is called out in the changelog.
The silent overrides drop
I put an esbuild override in package.json first, ran install, and the audit still flagged esbuild. The override had been silently dropped, because of the v11 field move above. Moving it to pnpm-workspace.yaml fixed it on the next install:
- # package.json
- "pnpm": {
- "overrides": { "esbuild": "0.28.1" }
- }
+ # pnpm-workspace.yaml
+ overrides:
+ "esbuild@0.28.0": "0.28.1"
The pnpm settings docs are the new home for overrides. git log -p package.json before the bump would have shown the pnpm block was already dead weight, but I only noticed when the audit refused to clear.
peerDependencyRules.allowedVersions is a maintenance pin
pnpm peers check failed on this project:
✕ unmet peer tailwindcss
Installed: 4.3.1
Wanted:
">=3.0.0 || >=4.0.0 || insiders":
@tailwindcss/typography@0.5.20
Tailwind 4.3.1 is inside >=4.0.0, so the peer is satisfied. pnpm’s strict peer check could not evaluate the range, because insiders is a Tailwind release tag, and pnpm expects a semver comparator there. @tailwindcss/typography@0.5.20 ships a peer range pnpm cannot parse.
The previous pnpm-workspace.yaml already had a workaround:
peerDependencyRules:
allowedVersions:
"@tailwindcss/typography>tailwindcss": "4.3.0"
I had read the exact version 4.3.0 as drift, since the installed version was 4.3.1, and removed it. That forced pnpm to try parsing the upstream range again, and the check failed. The rule is a maintenance pin for an upstream peer range pnpm cannot parse, and peerDependencyRules.allowedVersions is the documented escape hatch for exactly that.
The fix is to use a range instead of an exact version, so the next Renovate bump to 4.3.2 does not re-trip the same failure:
peerDependencyRules:
allowedVersions:
- "@tailwindcss/typography>tailwindcss": "4.3.0"
+ "@tailwindcss/typography>tailwindcss": "^4.3.0"
Verification
The version bump and the security additions landed in a handful of lines:
# mise.toml
- pnpm = "11.1.2"
+ pnpm = "11.7.0"
# pnpm-workspace.yaml
+ trustLockfile: true
# ci/src/index.ts (Dagger)
- .withExec(["npm", "install", "-g", "pnpm@11.1.2"])
+ .withExec(["npm", "install", "-g", "pnpm@11.7.0"])
+ { name: "audit-signatures", cmd: ["pnpm", "audit", "signatures"] },
package.json keeps engines.pnpm at >=11.7.0 <12. The packageManager field was still present at this point; I removed it later, once mise.toml became the single source of truth and Corepack kept drifting from it.
pnpm audit signatures passed clean. All 111 packages in the dependency tree carry valid ECDSA signatures from the npm registry. That is the expected result for a project that only uses npm-registry packages, but it is still worth verifying.
pnpm peers check passed clean as well, with the @tailwindcss/typography peer pin from above in place. Without that pin, the check fails on the unparseable insiders tag.
trustLockfile skips the re-verification pass on subsequent CI runs. I did not benchmark the difference on this project (the tree is small), but the release notes note a real memory reduction for large workspaces.
Next upgrade
I’d check the pnpm release page every month instead of waiting three months and six releases. The 11.4 supply-chain patches in particular were worth adopting immediately. A pnpm self-update run and a quick scan of the release notes takes two minutes and would have caught the 11.4 fixes when they shipped. I’d also read git log -p on pnpm-workspace.yaml before pruning a rule that looks like drift; the peerDependencyRules pin looked stale until the peers check failed without it.
References
- pnpm migration (v10 to v11). The settings move out of
package.json, and thepnpm-v10-to-v11codemod - pnpm settings. The new home for
overrides,peerDependencyRules, and related settings - pnpm
peerDependencyRules.allowedVersions. The escape hatch for upstream peer ranges pnpm cannot parse - pnpm 11.1 release notes.
audit signatures,pnpm bugs,pnpm owner,--no-runtime, arbitrary registries - pnpm 11.3 release notes.
trustLockfile, nativepnpm pkg/repo/set-script,pnpm stage - pnpm 11.4 release notes. Supply-chain vulnerability patches, tarball-integrity hard failures
- pnpm
audit signatures. ECDSA registry signature verification - pnpm
trustLockfile. Skip supply-chain re-verification for trusted lockfiles - pnpm
peers check. Lockfile peer dependency analysis - pnpm
devEngines.packageManager. Declarative pnpm version ranges - Supply Chain Security with mise and pnpm. My earlier post on the baseline security config
- Keeping Supply Chain Exclusions Honest. Audit workflow for decaying exclusion lists
- Corepack’s
packageManagerkept drifting frommise.toml. Why thepackageManagerfield was later removed - Dagger’s TypeScript SDK doesn’t understand pnpm catalog. Why Dagger modules live outside the workspace catalog
- Hardening a Renovate Config for Supply Chain Security. The Renovate config behind the version bumps
This post was written with AI assistance.