
pnpm 11 turns on supply-chain defenses by default — and it'll break your CI if you're not ready
pnpm 11 isn't just a version bump — three defaults flip from permissive to restrictive, each one aimed at the same place supply-chain attacks actually land: packages nobody reviewed, resolved from sources nobody vetted, running scripts nobody approved. If you're running pnpm on a small team — no dedicated platform engineer, no time to read a 40-page migration guide line by line — upgrading on a Friday afternoon and hoping for the best is exactly how this release turns into an afternoon of debugging why pnpm install suddenly fails in CI. Here's what actually changed, where the sharp edges are, and a rollout plan that doesn't require blocking the whole team.
What actually changed
Three settings ship with stricter defaults in v11:
minimumReleaseAge(default1440, in minutes — 24 hours): pnpm won't install a dependency version, direct or transitive, until it's been on the registry for at least this long. That's the cooldown — a malicious version published an hour ago can't get pulled into anyone's install before someone notices and yanks it.blockExoticSubdeps(defaulttrue): only the dependencies listed directly in your rootpackage.jsonmay resolve from a git repo or a tarball URL. Every transitive dependency has to come from a trusted source — your registry, a local path, a workspace link, or a trusted GitHub repo. A dependency three levels deep quietly pointing at someone's GitHub fork no longer flies.strictDepBuilds(defaulttrue): if any dependency has an unreviewed build or postinstall script,pnpm installnow exits with a non-zero code instead of just running it.
Put together: pnpm used to trust the registry and your dependency tree by default. v11 flips that — everything is unreviewed until you say otherwise.
The migration trap: five settings become one
The build-approval change is where most teams will actually get stuck, because it isn't additive — it's a replacement. v11 removes onlyBuiltDependencies, onlyBuiltDependenciesFile, neverBuiltDependencies, ignoredBuiltDependencies, and ignoreDepScripts outright and replaces all five with one setting, allowBuilds — a map of package-name patterns to true/false in pnpm-workspace.yaml:
allowBuilds:
esbuild: true
core-js: false
If any of those five old settings were in your config, they're silently ignored now — not an error, just gone — and any package with a postinstall script that isn't explicitly listed in allowBuilds counts as unreviewed. With strictDepBuilds: true as the default, your next clean install just exits non-zero. For a team running esbuild, sharp, native bindings, Playwright, or anything else with a build step, that's most installs.
The fix is one command, run once per repo: pnpm approve-builds. With no arguments it's interactive — it lists every pending package and lets you approve or deny each one. You can also skip the prompt: pnpm approve-builds esbuild fsevents !core-js approves the first two and explicitly denies the third (a ! prefix denies), or pnpm approve-builds --all approves everything pending in one shot if you trust your current tree. Either way, the decision gets written straight into allowBuilds in pnpm-workspace.yaml — commit that file so the rest of the team, and CI, inherit the same approvals instead of everyone hitting the same wall individually.
Where your .npmrc settings actually went
.npmrc isn't gone, but its job shrank: as of v11 it holds registry and auth settings only. Everything else moves to pnpm-workspace.yaml, or to the new global ~/.config/pnpm/config.yaml for machine-level settings. Concretely:
Per-scope registries (
@scope:registry=...lines) become aregistriesmap inpnpm-workspace.yaml.Per-project
.npmrcoverrides becomepackageConfigsinpnpm-workspace.yaml.The
node-mirror:<channel>setting becomesnodeDownloadMirrorsinpnpm-workspace.yaml.Network settings —
httpProxy,httpsProxy,noProxy,localAddress,strictSsl,gitShallowHosts— move to the YAML config too, though pnpm still reads them from.npmrcfor now to ease the transition.Environment variables switch from
npm_config_*topnpm_config_*; the old prefix is no longer read at all.
None of these fail loudly if you miss one — they just stop applying, which is worse than a hard error if you're relying on one for a private registry or an internal mirror. Worth a dedicated pass through your current .npmrc rather than assuming the upgrade carries it forward.
CI considerations: what stalls if you track latest
minimumReleaseAge is the one most likely to bite you first, and only in CI, which makes it extra fun to debug. If your pipeline resolves against a floating version — tracking latest, or a loose ^/~ range — and the version that would satisfy it was published inside the last 24 hours, pnpm won't resolve to it; that's the setting working as designed. Locally this rarely shows up, because you installed a day ago and your lockfile is already pinned. In CI, especially on a fresh runner with no cache, you can hit a brand-new release at exactly the wrong moment.
Two ways to handle it, and the right one depends on why you're floating in the first place: if you're intentionally tracking fresh releases — an internal package your own team ships multiple times a day, say — set minimumReleaseAge: 0 for that package, or workspace-wide if the cooldown doesn't suit your setup. If you're floating by accident because nobody got around to pinning, pin the version instead and let the cooldown do its job everywhere else. Either way, don't discover this failure mode in production CI first — run an install against a clean cache before you flip the upgrade on everyone.
A realistic rollout plan for a three-person team
Nobody on a three-person team has time to be “the pnpm 11 person” full time, so this is built to take an afternoon, not a sprint.
Pilot on one repo. Pick the smallest, least business-critical one you've got. Upgrade pnpm, run
pnpm approve-buildsinteractively so you actually see every package being approved, and commit the resultingpnpm-workspace.yaml.Audit .npmrc before touching the others. Now that you know what moved, go through the pilot repo's old
.npmrcline by line and confirm every setting has a new home, or confirm it genuinely was registry/auth and belongs where it is.Run a cold-cache CI install. Trigger a pipeline run without a warm cache before merging, so you catch a
minimumReleaseAgeresolution failure now, not three weeks from now at an inconvenient hour.Roll out repo by repo, not all at once. Copy the now-proven
pnpm-workspace.yamlpattern to the next repo rather than re-discovering the same five-settings issue there.Decide your minimumReleaseAge policy once, in writing, not per-repo. Zero-cooldown for internal fast-moving packages, the 24-hour default for everything external — and apply that consistently instead of re-deciding it under pressure every time CI fails.
The new defaults exist for a real reason, but a restrictive default you don't understand yet is just friction with better branding. Thirty minutes with the pilot repo turns pnpm 11 from a surprise into a checklist.
Sources:
