Skip to main content

npm v12 Turns Off 16 Years of Automatic Code Execution: The Exact Packages You Need to Re-Approve Before Your Build Breaks

9 min readDora NodaDora Noda
Share
On this page

On July 8, 2026, npm shipped v12 and quietly ended a 16-year-old assumption: that running npm install is a safe, side-effect-free way to fetch dependencies. It isn't, and hasn't been for years — it's just that npm finally stopped pretending otherwise. As of v12, npm install no longer runs a package's preinstall, install, or postinstall scripts, no longer resolves Git-URL dependencies, and no longer pulls remote tarballs, unless a project explicitly says so.

If that sounds like a small config flip, the numbers behind it aren't. Since September 2025, the self-propagating "Shai-Hulud" worm has moved through the npm registry in waves, starting with @ctrl/tinycolor (2 million-plus weekly downloads on its own) and eventually touching somewhere between 500 and 800 packages with combined weekly download counts north of 2.6 billion. May 2026 alone logged fourteen distinct npm/PyPI supply-chain campaigns spanning 346 packages — more than the previous four months combined, the single busiest month in a two-year tracking corpus. Nearly every one of those attacks used the same door: a postinstall script that ran automatically, the instant someone typed npm install. Socket's research puts a number on just how load-bearing that door was — 94% of malicious npm packages used at least one install script, even though install scripts are genuinely rare among legitimate ones. npm v12 closes it by default.

That's good news for anyone who's ever been paged over a compromised dependency. It's also a build-breaking change for anyone whose Node buildpack has spent a decade treating npm install as inert. Here's exactly what flipped, why it flipped now, and the concrete audit to run — which packages actually need their scripts back, and which projects were just quietly resting on a default they never should have had.

What Actually Changed

npm v12 flips three defaults that used to run silently. All three are visible as warnings in npm 11.16.0+ before v12 makes them hard failures, which is the version most 2026 migration guides tell teams to upgrade to first.

BehaviorPre-v12 defaultv12 defaultOpt back in
Lifecycle scripts (preinstall, install, postinstall, implicit node-gyp builds)Run automatically for every dependencyBlocked for every dependencynpm approve-scripts per package, recorded in an allowScripts block in package.json
Git-URL dependencies (direct or transitive)Resolved and cloned automaticallyRejected--allow-git
Remote tarball dependencies (https://...tgz sources)Fetched automaticallyRejected--allow-remote

The Git-dependency change closes a specific hole that --ignore-scripts never actually covered: a Git dependency's own .npmrc could override which Git executable npm invokes, achieving code execution even on installs that had scripts disabled. That's the detail worth sitting with — teams that thought they were already safe because their CI passed --ignore-scripts were still exposed through this path. v12 closes it structurally instead of relying on every team's install flags being correct.

The allowScripts block itself is unglamorous but load-bearing: it's a map of package name to true/false that npm writes into package.json when you approve a script, and it's meant to be committed like a lockfile. From v12 on, that block — not a flag anyone remembers to pass — is the actual security boundary between "this dependency's code runs on our build machine" and "it doesn't."

Why Now: A Year of the Same Door

npm v12 isn't a response to one incident — it's the ecosystem finally patching the pattern behind a full year of them:

  • September 2025 – ongoing: Shai-Hulud, the registry's first genuinely self-propagating worm. It phished a maintainer, stole their npm and GitHub tokens, republished itself into their packages with a malicious postinstall payload, and used the newly compromised packages' own maintainer credentials to keep spreading. Later waves reached packages connected to Zapier, PostHog, and Postman.
  • March 2026: A compromise centered on Axios-adjacent packages, using the same install-time execution path.
  • May 2026: 84 malicious versions published across the @tanstack/* scope — the same month that produced 14 separate supply-chain campaigns and 346 flagged packages overall, the worst single month on record.
  • June 2026: A compromise reaching the Mastra AI framework's package tree.

Different targets, same mechanism every time: a script that npm ran automatically, before a human ever reviewed what it did. That's the pattern 94% of malicious packages relied on, and it's the exact default npm v12 removes.

The Audit: What Needs Re-Approving, and What Doesn't

This is the part that actually determines whether your next deploy breaks. Upgrade to npm 11.16.0+, run a normal install, and read the warnings — that's your project's real list. But most Node codebases fall into a predictable shape, and it's worth knowing which side of it your dependencies land on before you're staring at a failed build.

Legitimately needs allowScripts re-enabled:

  • Native-module builds — anything with a binding.gyp that compiles C/C++ at install time: sharp, bcrypt, better-sqlite3, node-sass, node-canvas. These aren't malware; they're doing real work npm can no longer assume is safe by default. Note the sharper edge here: v12 blocks the implicit node-gyp build even for packages that have no explicit install script at all — having a binding.gyp file is itself enough to trigger the block.
  • Binary fetchers — packages whose "install" is really "download a prebuilt binary for this platform": esbuild, puppeteer, playwright, cypress. Skip approval here and your CI silently ends up with no browser binary or no bundler executable, failing at run time instead of install time — a worse debugging experience than a loud install failure.
  • Codegen and git-hook toolingprisma generate firing on install, husky wiring up Git hooks. These need scripts approved deliberately, since they're doing something real to the working tree, not just fetching a binary.
  • Monorepo bootstrap scripts — this is the case most audits miss. A root-level postinstall that runs nx run-many or a Turborepo/Lerna bootstrap step to sync generated code across workspace packages will silently stop firing under v12, and because it's declared at the workspace root rather than inside any single dependency, it's easy to overlook when you're scanning node_modules for offenders. It needs the same npm approve-scripts treatment as any dependency script — check your root package.json, not just your node_modules tree.
  • Transitive scripts, not just direct onesallowScripts doesn't care whether a scripted package is one you installed directly or one three levels down someone else's dependency tree pulled in. npm approve-scripts --allow-scripts-pending surfaces the full flattened list regardless of depth, which is the only reliable way to catch a nested esbuild or sharp a workspace hoisted somewhere you weren't looking.

Quietly relying on a default they never needed:

  • Packages that ship a postinstall purely to print a funding nag, an opt-out telemetry notice, or a "thanks for installing" message. These have always been vestigial and cost nothing to leave blocked.
  • Projects that copy-pasted a --ignore-scripts=false override into .npmrc years ago to fix one broken install, then never revisited it — v12 forces that decision to be made per-package instead of globally, which is strictly safer even though it's more annoying up front.

The actual migration commands are short:

bash
npm approve-scripts --allow-scripts-pending   # see everything currently pending approval
npm approve-scripts --all                     # approve everything you've reviewed and trust
# commit the resulting allowScripts block in package.json like a lockfile

In CI, add --strict-allow-scripts after your approvals are committed, so a new transitive dependency that shows up with an unapproved script fails the build loudly instead of silently skipping work it needed. There's also a --dangerously-allow-all-scripts escape hatch documented for migration — treat it as exactly what the flag name says. Using it in CI to "make the red X go away" reintroduces the exact blast radius npm v12 exists to remove.

What This Means for a Git-Push PaaS's Node Buildpack

Every buildpack that auto-detects a Node app and runs npm install on a tenant's behalf has been quietly resting on the same assumption npm just revoked: that install is safe to run unattended. A buildpack built before v12 has three ways to react, and only one of them is actually correct.

The wrong fix is defaulting the build step to --dangerously-allow-all-scripts to keep existing tenant apps building without a support ticket. That silently restores the exact attack surface npm v12 removed — a compromised transitive dependency's postinstall runs on the platform's own build infrastructure again, just with the warning suppressed instead of heeded.

The right fix has two parts. First, a buildpack has to surface npm's warnings and failures to the tenant as a real build-log message — "this dependency has an unapproved install script" — instead of swallowing them the way a build step that only checks the exit code would. Second, it needs a documented, git-committed path for a tenant to ship their own allowScripts approvals as part of their repo, the same way they'd commit a lockfile — so sharp or esbuild in a real app keeps working without the platform ever having to decide, on the tenant's behalf, which scripts are safe to run.

That's the version of "zero-config" a build layer should still offer after this change: the platform doesn't run untrusted code by default any more than npm itself does, and the tenant — not a blanket override baked into the buildpack — is the one who decides which dependency earns an exception.

The New Baseline

npm v12 isn't really an npm story. It's the first major package manager to conclude, in public and by default, that "download and immediately execute" was never a defensible default for third-party code — a lesson every other ecosystem with an install-time hook (PyPI's setup.py, RubyGems' extension builds) is watching happen in real time. For a self-hosted platform whose entire pitch is that a tenant should be able to see and trust every step between git push and a running service, treating the build step's own dependency install as historically trustworthy was always the weakest link in that chain. npm just made not trusting it the default. A buildpack's job now is to not quietly undo that.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex