A default that changes how every Node project installs dependencies
On July 1, 2026, npm released v12 with what the npm team describes as the most significant security redesign in the package manager's ~16-year history. The change is simple in concept but broad in scope: by default, npm now blocks lifecycle scripts — specifically postinstall, preinstall, prepare, and others — from executing during npm install. It also blocks Git dependencies and arbitrary remote sources by default. No opt-in flag, no gradual rollout. This is a breaking change that affects every Node.js project the moment a team upgrades.
The trigger is not theoretical. Between March and July 2026, npm supply-chain attacks escalated sharply. High-profile packages including Axios and Mastra AI were compromised, with malicious code executing via postinstall hooks. On July 14 alone, Microsoft Threat Intelligence disclosed a coordinated attack against the @asyncapi npm organization — five malicious package versions across four package names (@asyncapi/specs, @asyncapi/generator, @asyncapi/generator-components, @asyncapi/generator-helpers) republished within approximately 90 minutes, executing code at module-load time. The vulnerability class is ancient: for over a decade, running npm install automatically granted every package in a dependency tree — including deeply nested transitive dependencies a developer never directly chose — the ability to execute arbitrary shell commands on the installing machine.
npm v12 closes this by defaulting to deny, not allow. The question every team asks now is the same one we're hearing from clients: which of our dependencies actually need those scripts, how do we safely allowlist them, and what breaks in our CI pipelines during the migration?
Knowing what genuinely needs install scripts vs. what doesn't
The first instinct is wrong. Not every package needs lifecycle scripts re-enabled. Most don't. The common misconception is that npm v12 will "break" a lot of packages — actually, most packages neither use nor need install scripts at all. The packages that do need them fall into a small category: those that compile native code.
Packages like sharp (image processing via libvips), sqlite3, node-gyp-based modules, and canvas need to build native addons during installation. That compilation happens in a postinstall script. A few other packages use prepare scripts to transpile TypeScript or bundle code, though modern practice is to ship pre-built distributions instead. If a package author forces a build-time compilation step, they have a good reason — almost always a native binding or a runtime requirement that can't be baked into the distribution.
To audit your dependencies, the approach is straightforward:
- Run
npm lsto list all your direct and transitive dependencies. - For each package, inspect its
package.jsoninnode_modules/— specifically thescriptssection. Look forpostinstall,preinstall,prepare, orpostuninstallentries. - Cross-check against your application's runtime needs. If a package compiles a C++ extension, you'll need it. If it's just running a minifier or test setup, you don't.
A practical starting point: audit your direct dependencies (the ones in your own package.json) first. Those are the ones you consciously chose and should understand. Transitive dependencies are secondary — npm v12's new allowlist mechanism handles scoping.
Config: The safe allowlist pattern
npm v12 ships with the --allow-scripts flag and a per-package allowlist mechanism via .npmrc. Here's a realistic pattern for a production Next.js project:
# .npmrc
# Allow scripts only for packages that genuinely need native compilation.
# Note: This whitelist must be maintained as dependencies update.
allow-scripts=false
# Image processing — sharp compiles libvips bindings
allow-scripts:sharp=true
# SQLite bindings for local dev databases (if you use this)
allow-scripts:sqlite3=true
# Playwright browser automation (if your test suite needs it)
allow-scripts:playwright=true
# Native Node.js bindings for cryptography (if you use node-crypto or similar)
allow-scripts:node-gyp-less-than-4=true
# ESBuild for build acceleration (if using native bundle)
allow-scripts:esbuild=true
The key principle: name the exact package and the specific version (or version range) you're allowing. Be explicit about scope. Never blanket-allow all scripts with allow-scripts=true. That defeats the entire purpose.
For monorepos or projects with multiple package managers, the pattern scales. Place .npmrc allowlists at the workspace root and, if needed, at individual workspace roots. Test that your configuration is correct by running npm install --dry-run — this will show you which scripts npm would execute before actually executing them.
What breaks in Docker builds and CI/CD
The most immediate pain point: Docker. A Dockerfile using npm install or npm ci now fails silently by default if the image depends on packages requiring scripts:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
# This will block scripts for sharp, esbuild, etc.
RUN npm ci
COPY . .
RUN npm run buildIf your build depends on sharp for image optimization or esbuild for native bundling, the image build succeeds (no error), but the runtime is incomplete. The compiled native bindings are missing. You discover the failure at runtime or in production.
The fix: add an .npmrc to your Docker context or pass config at build time:
FROM node:22-alpine
WORKDIR /app
# Copy allowlist before installing dependencies
COPY .npmrc ./
COPY package*.json ./
# npm respects the .npmrc in the working directory
RUN npm ci
COPY . .
RUN npm run buildGitHub Actions and similar CI systems work the same way. If your workflow runs npm install or npm ci, add the .npmrc to the repo and ensure it's checked in. Alternatively, use environment variables to configure npm at runtime:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- run: npm ci
env:
NPM_CONFIG_ALLOW_SCRIPTS: 'false'
NPM_CONFIG_ALLOW_SCRIPTS_SHARP: 'true'
NPM_CONFIG_ALLOW_SCRIPTS_ESBUILD: 'true'Note: GitHub Actions' cache: npm already handles .npmrc, so the explicit allowlist is picked up automatically if you commit it.
Broader defense-in-depth: beyond install scripts
Blocking scripts is one layer, but npm v12 is part of a larger picture. A robust supply-chain posture includes:
Lockfile integrity. Your package-lock.json (or pnpm-lock.yaml, yarn.lock) is your source of truth. Commit it, review changes carefully, and never auto-merge lockfile diffs in CI. A malicious actor who modifies the lockfile can pivot to any attack — script blocks become irrelevant.
Minimal transitive trust. Fewer dependencies mean fewer attack surfaces. Before adding a package, ask: do we truly need this, or can we write 50 lines of code instead? Packages with large dependency trees inherit risk from all of them. Tools like npm ls and npm audit help, but they're reactive. Proactive minimization is stronger.
Software Bill of Materials (SBOM). For projects serving regulated industries or requiring compliance, maintaining an SBOM — a list of all dependencies with their versions and known vulnerabilities — is standard. Tools like cyclonedx or syft generate these from your lockfile. If a dependency is compromised and you have an SBOM, you know exactly which projects are affected and can act faster than competitors.
Signed commits and verified publishes. If your npm packages are public, sign your releases and configure npm to require signature verification. The ecosystem is still early on this (adoption is ~5% of the public registry), but it's becoming standard for security-conscious projects.
Takeaways
- npm v12's script block is a default-deny security posture — most packages don't need scripts, and audit will tell you which ones do.
- Packages requiring native compilation (sharp, sqlite3, node-gyp modules) need explicit allowlist entries in
.npmrc. Audit your direct dependencies first. - Docker builds and CI/CD pipelines need
.npmrcallowlists checked into the repo or configured via environment variables. Test with--dry-runbefore deploying. - A strong supply-chain defense includes lockfile integrity, minimal dependency counts, SBOMs for regulated projects, and (where possible) signed releases.
- The migration to npm v12 is not "all or nothing." You can audit, allowlist, and roll out incrementally — dev environments first, then CI, then production.
References: npm v12's July 2026 security redesign announcement, public reporting on 2026 npm supply-chain attacks (Axios, Mastra AI), Microsoft Threat Intelligence's July 14, 2026 disclosure of the @asyncapi npm organization compromise, and best practices from the npm security documentation.