Loading blog posts...
Loading blog posts...
Loading...
npm v12's security shift blocks install scripts by default, breaking builds that depend on native binaries. Learn how to audit your dependencies and update your CI pipeline before it fails.

npm v12 shipped on July 8, 2026, and it quietly broke a long-standing assumption: npm install no longer runs whatever code package authors include by default. Install scripts are now off unless you explicitly approve them. If your CI pipeline depends on packages like sharp, esbuild, or node-sass, there’s a good chance a build failure is already queued up. The tricky part is that missing native binaries don’t always fail the build. You can get a green CI run that only blows up later, at runtime.
The July 8 changelog confirms that npm v12 flips install-time script execution from automatic to explicit approval. This goes beyond the usual postinstall scripts. Here’s what’s blocked by default now:
| Execution Type | Previous Behavior | npm v12 Default |
|---|---|---|
preinstall, install, postinstall scripts | Auto-run | Blocked |
binding.gyp triggered node-gyp builds | Auto-run | Blocked |
prepare scripts for git/file/link deps | Auto-run | Blocked |
| Direct git dependencies | Allowed | allow-git=none |
| Remote URL/tarball sources | Allowed | allow-remote=none |
The binding.gyp change is the one that tends to surprise teams. A package doesn’t need an obvious postinstall script to execute code during install. If it includes a binding.gyp, node-gyp used to kick off a build automatically. That path is now shut.
Warning
Packages with binding.gyp files may install without errors but produce non-functional binaries. Your CI will show green until tests or production actually try to use the native module.
The 2025-2026 attack numbers made “wait and see” unrealistic. Sonatype's 2026 report flagged over 454,600 new malicious packages in 2025, with more than 99% coming from npm. Q4 2025 alone reported 394,877 new malware packages: a 476% increase compared to the prior three quarters combined.
Two incidents, in particular, pushed this over the line. The September 2025 “Shai-Hulud” worm injected malicious postinstall scripts into popular packages, which ended in more than 500 compromised packages being removed. The sneakier one was “Miasma” (also called “Phantom Gyp”), which Chainguard documented as hitting 57 packages across 286 malicious versions. Miasma didn’t rely on obvious postinstall scripts. It abused binding.gyp and node-gyp behavior to run code without tripping the usual alarms. Scanners that only looked for suspicious postinstall scripts missed it.
Why it matters: The risk wasn’t limited to “packages with postinstall.” It was any dependency that could trigger native compilation. npm v12 blocks that entire attack path by default.
This is where upgrades to npm v12 can get ugly. Blocking an install script doesn’t necessarily make npm ci fail.
bashnpm ci # ✅ Exit code 0 # ✅ node_modules populated # ❌ Native binaries missing or non-functional
npm ci can still succeed because the JavaScript parts install normally. The native build step that used to run during install simply never happens. Your pipeline keeps moving. Your tests might even pass if they don’t hit the native code paths. Then production tries to resize an image with sharp, and suddenly it’s clear the binary was never built.
javascript// This import succeeds const sharp = require('sharp'); // This fails at runtime sharp('input.jpg').resize(300, 200).toFile('output.jpg'); // Error: Could not load the "sharp" module using the linux-x64 runtime
To catch this early, you need smoke tests that actually exercise native modules, not just import them.
javascript// ci-smoke-test.js const sharp = require('sharp'); const canvas = require('canvas'); // Actually use the native bindings await sharp(Buffer.from([0x89, 0x50, 0x4e, 0x47])).metadata; console.log('sharp: OK'); const ctx = canvas.createCanvas(1, 1).getContext('2d'); console.log('canvas: OK');
Run something like this after npm ci to catch silent failures before they hit production. What’s often missed: importing a native module can succeed even if the binary underneath is broken. You need to call into it.

npm v12 adds a project-level approval workflow through the new allowScripts field in package.json. The approve-scripts documentation walks through the workflow end to end.
bash# Step 1: Identify packages with unreviewed install scripts npm approve-scripts --allow-scripts-pending
This prints packages that have install scripts but aren’t on your allowlist yet. Review each one carefully. In most cases, the surprises are transitive dependencies your team didn’t choose directly.
bash# Step 2: Approve trusted packages npm approve-scripts sharp npm approve-scripts esbuild npm approve-scripts @prisma/client
Approvals are version-pinned by default. So if sharp releases a new version, it won’t inherit approval automatically. That’s the point: it forces a deliberate re-check.
json{ "allowScripts": { "sharp@0.33.4": true, "esbuild@0.21.5": true, "@prisma/client@5.15.0": true, "suspicious-package@1.0.0": false } }
The version pinning is doing the real security work here. If an attacker compromises a maintainer account and ships a malicious update, that update won’t automatically get code execution in your environment. It will show up in --allow-scripts-pending, and your team can investigate before approving.
Tip
Run npm approve-scripts --allow-scripts-pending in CI as a separate step. Fail the build if any unapproved packages appear. This catches new dependencies added by PRs before they can execute code.

These categories typically need explicit approval to work correctly:
Native image processing: sharp, canvas, jimp (with native bindings), imagemagick
Build tools with native components: esbuild, swc, node-sass, sass (with native compiler)
Database drivers: better-sqlite3, sqlite3, pg-native, oracledb
Cryptography: argon2, bcrypt, sodium-native
System integration: fsevents (macOS file watching), node-pty, serialport
Monorepo tooling: Packages using prepare scripts for git dependencies
yaml#.github/workflows/ci.yml - name: Install dependencies run: npm ci - name: Verify native modules run: node scripts/verify-native-modules.js - name: Run tests run: npm test
That verification step between install and test is what prevents the “green build, broken runtime” situation. Make the verification script import and actually use each native module your app depends on.
npm v12’s install security changes land alongside another breaking shift: 2FA-bypass granular access tokens (GATs) are being phased out.
| Milestone | Date | Impact |
|---|---|---|
| GATs lose sensitive management capabilities | August 2026 | Can't manage accounts, packages, or orgs |
| GATs lose publishing ability | ~January 2027 | Must use trusted publishing |
The replacement is trusted publishing via OIDC. Instead of long-lived tokens sitting in CI secrets, the workflow requests short-lived credentials and publishes with provenance attestations.
yaml## GitHub Actions trusted publishing jobs: publish: permissions: id-token: write contents: read steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '22' registry-url: 'https://registry.npmjs.org' - run: npm publish --provenance --access public
The id-token: write permission lets the workflow request an OIDC token. npm checks it against your trusted publisher config, issues a short-lived credential, and attaches provenance metadata to the published package.
No more long-lived tokens living in repository secrets. No more uncertainty about whether a token created years ago is still floating around somewhere.
Why it matters: Put together, install script approvals and trusted publishing shift npm to a different security model. Code execution needs explicit approval. Publishing needs cryptographic proof that it came from the right CI pipeline.
npm 11.16+ ships these behaviors behind warnings, so teams can start moving now without fully switching to npm v12.
bash## Enable npm v12 behavior in npm 11.16+ npm config set allow-scripts false npm config set allow-git none npm config set allow-remote none
Run your full CI pipeline with those settings. See what breaks. Build out allowScripts. Then remove the config overrides and commit the package.json updates.
bash## Audit current dependencies npm approve-scripts --allow-scripts-pending > scripts-to-review.txt # Review each package cat scripts-to-review.txt | while read pkg; do npm view "$pkg" repository.url npm view "$pkg" scripts done
The goal is simple: have allowScripts finished and tested before npm v12 becomes the default in your CI environment. Most CI providers tend to update Node.js images pretty quickly after a major npm release.
Important
Commit your allowScripts configuration to version control. Local and CI environments must stay synchronized, or you'll have "works on my machine" failures in production deployments.
Start here
Run npm approve-scripts --allow-scripts-pending on your main project and look for install scripts your team has never reviewed.
Quick wins
Deep dive
allowScripts entries for version pinning and avoid overly broad approvalsnpm v12 is a shift from implicit trust to explicit approval. For more than a decade, the JavaScript ecosystem worked on the assumption that package authors could run arbitrary code during installation. That made plenty of legitimate workflows possible, but it also helped support a world where 454,600 malicious packages can show up in a single year.
There’s a real short-term cost: broken builds, migration effort, and extra CI steps. The upside is long-term: code execution now requires a conscious decision, not just npm install.
Teams that start now, by building their allowScripts lists and adding native-module verification in CI, will usually transition cleanly. Teams that wait until npm v12 hits their production pipeline are the ones most likely to spend a painful week untangling “everything passed, but nothing works.”