~/blog

npm v12 stopped running your postinstall scripts

published

#npm#node#security

TL;DR

npm v12 is generally available and ships three defaults that used to be the opposite. Dependency lifecycle scripts — preinstall, install, postinstall — and implicit node-gyp builds no longer run unless the package is on an allowlist. Git dependencies and remote-URL tarballs no longer resolve at all without a flag.

Snapshot what you already trust, commit it, then trim:

npm install
npm approve-scripts --all
git add package.json && git commit -m "chore: snapshot install-script allowlist"

The allowlist is a field in package.json, not a lockfile entry, so it reviews like code.

The problem

The failure is quiet. npm install exits 0, and the thing that used to appear after installing does not appear: no downloaded binary, no compiled native module, no generated file. The error only surfaces later, at runtime, from a package that assumed its postinstall had run.

Two shapes of this are common. A native module that compiles through node-gyp — the build simply does not happen. And a package that downloads a platform binary during install, which then fails when your code goes looking for it.

npm does tell you, at the end of the install: it lists the packages whose scripts were skipped. That list is the whole interface. If your CI collapses install output, you will never see it.

Why it happens

GitHub’s framing is that dependency lifecycle scripts are the single largest code-execution surface in the npm ecosystem — arbitrary code from every transitive dependency, executing on npm install, before anything has been reviewed. v12 closes that path by default, plus the two adjacent ones.

BehaviorBefore v12In v12
Dependency preinstall/install/postinstallruns automaticallyskipped unless in allowScripts
Implicit node-gyp buildruns automaticallyskipped unless allowed
Git dependencies (direct or transitive)resolvedblocked; --allow-git defaults to none
Remote URL dependencies (https tarballs)resolvedblocked; --allow-remote defaults to none

Your own package’s scripts are not the target here — this is about code arriving from dependencies.

What to do

Start by looking, not approving. --allow-scripts-pending is read-only; it lists every package whose install scripts are not yet covered:

npm approve-scripts --allow-scripts-pending

Approve specific packages once you have decided each is one you actually want executing code:

npm approve-scripts sharp better-sqlite3

Entries are pinned to the installed version by default ([email protected]), which is the behavior you want: a version bump re-enters the review queue instead of inheriting approval. --allow-scripts-pin controls that if you disagree.

npm approve-scripts --all approves everything currently pending in one go. That is a migration tool, not a policy — it says “whatever I already had installed, keep working”. Run it, commit the diff, then delete the lines you cannot justify.

Git and remote-URL sources are governed separately from scripts, by --allow-git and --allow-remote. Both default to none, so a dependency pulled from a git URL or an https tarball — including a transitive one — stops resolving until you permit it explicitly. Auditing why one is there is usually a better use of the afternoon than allowing it.

If you are still on npm 11, you can rehearse the whole thing before upgrading. The features landed in 11.16.0, and --strict-allow-scripts turns on v12’s enforcement early:

npm install --strict-allow-scripts

That is the cheapest way to find out which CI job breaks, on your schedule instead of npm’s.

While you are in there: the 2FA-bypass token change

The same release note carries a second deadline that hits publishing rather than installing. Since early August 2026, npm granular access tokens configured to bypass 2FA can no longer perform sensitive account operations — creating or deleting tokens, changing password/email/2FA settings, changing package access or maintainers, or managing org and team membership. Those now require an interactive 2FA challenge.

Around January 2027, those tokens lose direct publishing entirely. Publishing moves to trusted publishing (OIDC) or staged publishing, where a human approves the release with 2FA. GitHub personal access tokens, GitHub App tokens and GITHUB_TOKEN in Actions are not affected by this change.

If a release pipeline currently holds a long-lived npm publish token, that pipeline has a rewrite due, and OIDC removes the token instead of rotating it.

Caveats

References