# pnpm vs npm Security in 2026: Install Scripts and Release Age, Tested

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:

```json
{
  "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:

```text
npm warn install-scripts 1 package has install scripts not yet covered by allowScripts:
npm warn install-scripts   install-canary@1.0.0 (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:

```text
Ã— 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 `esbuild@0.28.2`, 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:

```bash
npm approve-scripts esbuild
node -p "JSON.stringify(require('./package.json').allowScripts)"
# {"esbuild@0.28.2":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`:

```yaml
allowBuilds:
  esbuild: true
```

With that file in place, `pnpm add esbuild@0.28.2` 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](https://pnpm.io/supply-chain-security). npm's [`min-release-age`](https://docs.npmjs.com/cli/v11/using-npm/config/) 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`:

```ini
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`:

```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-lockfile` in 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](https://fabwaseem.com/blog/nextjs-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](https://fabwaseem.com/blog/ssrf-protection-nodejs) 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 `ai@7.0.159` 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](https://fabwaseem.com/blog/pnpm-vs-npm-security).*
