pnpm vs npm Security in 2026: Install Scripts and Release Age, Tested
Cross-posted from my blog. The original, with any updates, lives on fabwaseem.com. Most npm malware runs the moment you install it, through a postinstall script, and it usually ships as a fresh patch release that gets p
Cross-posted from my blog. The original, with any updates, lives on fabwaseem.com.
Most npm malware runs the moment you install it, through a postinstall script, and it usually ships as a fresh patch release that gets pulled within hours or days. So the two package-manager settings that matter most for supply chain security are: does it run dependency install scripts by default, and will it install a version that was published an hour ago. I tested both on pnpm vs npm tonight with a harmless canary package and a real package that had just shipped a release. The defaults are not what most people assume, and they differ between npm 11 and npm 12.
pnpm vs npm security: results with default settings
Tested November 3, 2026 on Linux with Node 26.13.0.
| npm 11.20.0 (bundled with Node 26.13) | npm 12.2.1 (latest) |
pnpm 12.16.1 | |
|---|---|---|---|
Dependency postinstall script |
ran, with a warning | blocked, with a warning | blocked, install exits with an error |
| Installs a version published 17 hours ago | yes | yes | no (waits 1 day) |
| Approve a package's scripts | npm approve-scripts <pkg> |
npm approve-scripts <pkg> |
allowBuilds in pnpm-workspace.yaml
|
| Release-age setting |
min-release-age (days, off by default) |
min-release-age (days, off by default) |
minimumReleaseAge (minutes, default 1440) |
Node 24.22.2 bundles npm 11.19.1, which has the same script settings as 11.20. If you use the npm that came with Node, you're on npm 11.
Test 1: does the install script run?
I made a local package whose only job is to write a file when its postinstall runs:
{
"name": "install-canary",
"version": "1.0.0",
"scripts": {
"postinstall": "node -e \"require('fs').writeFileSync('/tmp/canary','ran')\""
}
}
Then I packed it with npm pack and installed the tarball into an empty project with each tool, deleting /tmp/canary before each run.
npm 11.20.0 printed a warning and then ran the script anyway. /tmp/canary existed afterwards:
npm warn install-scripts 1 package has install scripts not yet covered by allowScripts:
npm warn install-scripts [email protected] (postinstall: node -e "...")
npm warn install-scripts Run `npm install-scripts ls` to review, or `npm install-scripts approve <pkg>` to allow.
npm 12.2.1 printed a similar warning, but worded as "had install scripts blocked", and the canary file was not created.
pnpm 12.16.1 didn't run the script and exited with code 1:
× adding a new package
╰─▶ Ignored build scripts: install-canary@file:../canary/install-canary-1.0.0.tgz
help: Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.
The packages were still written to node_modules, but the failing exit code means CI stops instead of quietly continuing. pnpm's docs call this strictDepBuilds, and it's on by default.
I got the same pattern with [email protected], a real package with a legitimate postinstall: npm 11 ran it with a warning, npm 12 blocked it, and pnpm blocked it and failed.
Allowing the scripts you trust
With npm (11.19+ and 12), approve per package. The approval is pinned to the exact version:
npm approve-scripts esbuild
node -p "JSON.stringify(require('./package.json').allowScripts)"
# {"[email protected]":true}
Because the approval is pinned, a new esbuild release will be flagged again, which is exactly when you want to look.
With pnpm, list trusted packages in pnpm-workspace.yaml:
allowBuilds:
esbuild: true
With that file in place, pnpm add [email protected] finished with exit code 0. pnpm approve-builds gives you an interactive picker for the same thing.
Test 2: will it install a brand-new release?
The ai package published 7.0.162 at 01:19 UTC on November 3, about 17 hours before I ran this. The previous releases were 7.0.161 and 7.0.160 on November 2, and 7.0.159 on October 30.
| Command | Installed |
|---|---|
npm i ai (npm 11.20.0) |
7.0.162 |
npm i ai (npm 12.2.1) |
7.0.162 |
npm i ai --min-release-age=1 (npm 11.20.0) |
7.0.159 |
pnpm add ai (pnpm 12.16.1, defaults) |
7.0.159 |
Both npm versions installed the newest release by default. pnpm skipped everything younger than 24 hours because minimumReleaseAge defaults to 1440 minutes, as described in pnpm's supply chain security docs. npm's min-release-age does the same once you set it, but it's measured in days and is off by default.
Note that the cooldown cuts both ways. A real security fix published today also waits. Both tools let you exempt packages: min-release-age-exclude in npm, minimumReleaseAgeExclude in pnpm. With minimumReleaseAgeExclude: [ai] in pnpm-workspace.yaml, pnpm installed 7.0.162 straight away.
Recommended settings for each
npm
In the project's .npmrc:
min-release-age=3
strict-allow-scripts=true
Then approve the few packages that genuinely need install scripts with npm approve-scripts <pkg>, and commit the allowScripts field in package.json. If you're still on npm 11, upgrading to npm 12 gets you blocking by default: npm i -g npm@12.
pnpm
In pnpm-workspace.yaml:
minimumReleaseAge: 4320 # 3 days, in minutes
trustPolicy: no-downgrade # refuse a version with weaker publish provenance than earlier ones
allowBuilds:
esbuild: true
sharp: true
Script blocking, strictDepBuilds and blockExoticSubdeps (no git or tarball URLs deep in the tree) are already on by default.
Both
-
Commit the lockfile and install with
npm ci/pnpm install --frozen-lockfilein CI. The release-age window matters when versions get resolved, which is when the lockfile changes, so review lockfile diffs in pull requests. - Don't run installs with secrets in the environment. If a script does slip through, it can read whatever env vars the install step has.
- Build Docker images from the lockfile. My Next.js Dockerfile installs from the lockfile in a separate deps stage.
If you write server code that fetches user-supplied URLs, SSRF protection in Node.js covers the other half of keeping a Node app safe.
So, is pnpm more secure than npm?
With default settings, yes, in both tests: pnpm blocked the install script and failed the install, and it refused a 17-hour-old release. npm 12 now blocks install scripts by default too, but still installs brand-new releases unless you set min-release-age. npm 11, which is what ships with Node 24 and Node 26 today, only warns about install scripts. Configured properly, npm 11.19+ and npm 12 can match pnpm on both points. The difference is what you get if you never touch the config.
FAQ
Does pnpm run postinstall scripts?
Not for dependencies, unless you allow them. pnpm 12.16.1 skipped my canary's postinstall and exited with Ignored build scripts. Allow trusted packages with allowBuilds in pnpm-workspace.yaml or by running pnpm approve-builds.
Does npm block install scripts by default?
npm 12 does: version 12.2.1 blocked my canary's postinstall. npm 11.20.0, the version bundled with Node 26.13.0, only printed a warning and ran the script. Set strict-allow-scripts=true or upgrade to npm 12.
What is minimumReleaseAge in pnpm?
It's the number of minutes a version must have been published before pnpm will install it. It defaults to 1440 (one day). In my test, pnpm installed [email protected] instead of the 17-hour-old 7.0.162.
What is min-release-age in npm?
An npm config option, measured in days, that makes npm ignore versions published more recently than that. It's off by default. npm i ai --min-release-age=1 installed 7.0.159 instead of the 17-hour-old 7.0.162.
Is pnpm safer than npm?
With default settings, pnpm blocked more in my tests: it refused to run dependency install scripts and refused a release under 24 hours old. npm can be configured to do both with strict-allow-scripts and min-release-age.
Originally published on fabwaseem.com.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.