On 8 July 2026, npm v12 shipped with install scripts disabled by default — closing what GitHub had called the ecosystem's largest code-execution surface.
On 4 August 2026, a worm propagated across 444 npm package names using a preinstall hook.
Four weeks. The obvious question is how, and the answer is more useful than the incident itself.
A default is not enforcement
npm 12 changed the default value of a setting. It did not remove the capability, and it could not have — a meaningful fraction of the ecosystem legitimately requires install scripts to build native modules.
So the hook still fires in every one of these situations:
You are not on npm 12. Pipelines pin npm versions. Base images ship whatever npm their Node version bundled. actions/setup-node resolves what the workflow file requests. A large share of CI in the world is running npm 10 or 11 and will be for a long time.
Something re-enabled it. Native dependencies fail to build without scripts, so teams that hit that wall set ignore-scripts=false in .npmrc — usually once, globally, years ago, to fix one package. That file is committed, inherited, and forgotten.
You are not using npm. Yarn, pnpm, and Bun have their own defaults and their own flags. An organisation that standardised on pnpm got no protection from an npm release note.
A wrapper resolved differently than you think. Monorepo tooling, Docker layers that invoke a different package manager than the local one, and vendored install steps all decide this independently of your intent.
The generalisable form: an ecosystem default protects the median user, not your specific pipeline. Whether it protects you is a property of your configuration, and it is empirically checkable — which is the point most teams skip.
Check it, do not assume it
The verification is cheap:
# What npm is CI actually running?
npm --version
# What is the effective setting, after all .npmrc layers?
npm config get ignore-scripts
# Where did that value come from?
npm config list -l | grep -i ignore-scripts
Run these inside the CI job, not on a developer laptop. The laptop's answer is not evidence about the runner, and the runner is where the credentials live.
For enforcement rather than defaults, set it explicitly at the highest layer you control:
# In CI, be explicit rather than relying on the version default
npm ci --ignore-scripts
Then handle the packages that genuinely need to build natively as named exceptions, rather than by disabling the control globally. An allowlist of six packages that may run scripts is a security posture; ignore-scripts=false is the absence of one.
Why attackers adapted in days, not months
We wrote at the end of July that npm v12 disabled install scripts and attackers adapted in three days, moving the jscrambler payload to execute on import instead of on install.
The keyv campaign shows the other half of that adaptation: you do not always need a new technique when the old one still works on most of the population. Install-script execution remains viable across the large installed base of older npm versions and re-enabled configurations. Attackers do not need every target; they need enough of them, and a worm needs only enough publish tokens to keep propagating.
This is the practical lesson about one-vector mitigations. They shift the population distribution rather than eliminating the vector, and the residual population is large enough to sustain a campaign for years.
Execution surfaces beyond install scripts
If you disable install scripts and consider dependency code-execution solved, the remaining surfaces are worth naming:
Import-time execution. Module top-level code runs when you require or import it. Your test suite imports your dependencies. This is where the jscrambler payload moved.
Build-time execution. Bundlers, transpilers, and their plugin ecosystems execute arbitrary plugin code at build time. A malicious Babel or Vite plugin does not need an install hook.
Postinstall in transitive dependencies. Your direct dependency may declare no scripts while something four levels down does.
Runtime, in production. The most obvious one, and the one most people mean by "supply chain attack" without realising the other three come first chronologically.
Disabling install scripts closes the earliest and easiest surface. It does not close the category, and treating it as a solved problem is how organisations end up surprised by an import-time payload.
What actually reduces this risk
Remove the prize. The keyv payload harvested npm tokens, GitHub tokens, AWS credentials, and Vault secrets from build environments — then used the npm tokens to propagate. If your CI holds no long-lived publish credential, arbitrary execution in a build step is a bad day rather than an ecosystem event. OIDC trusted publishing is the concrete mechanism.
Constrain egress from builds. The payload had to fetch a Bun runtime and post credentials outward. Both fail against an allowlisted egress policy. This is the single most underused control in CI.
Isolate the install step. Dependency installation should run with fewer privileges and less network reach than the build that follows it, and neither needs production credentials.
Record what installed, when. The first question in every one of these incidents is which builds ran during the malicious window. Build-time dependency records answer it in minutes.
How Safeguard helps
Build-time SBOM with timestamps. Safeguard's Supply Chain Core records exact resolved versions per build, so "did any pipeline install an affected version between the malicious publish and the takedown" is answerable from existing data rather than reconstructed from logs that may have rotated.
Behavioural delta analysis across versions. A package version that newly declares a preinstall hook, newly downloads an external runtime, or newly touches credential paths is anomalous on its face. Because no CVE is ever issued for malicious-by-design code, version-over-version behaviour is the only signal that exists on the timescale these campaigns operate on.
Configuration policy for the pipeline itself. Safeguard checks the controls you believe are in place — script execution settings, lockfile integrity, registry configuration, token type — against what your pipelines actually run, so an inherited .npmrc that quietly re-enabled scripts surfaces as a finding rather than as an incident.
Lion for credentials and egress. Just-in-time secret brokering leaves no resident tokens for a payload to harvest, capability scoping bounds what a compromised build step can reach, egress allowlists break both the second-stage fetch and the exfiltration, and signed audit trails make the window reconstructable.
Go and print npm config get ignore-scripts from inside your CI job. If you have not seen that output, you do not know whether the July default applies to you.
Sources: Aikido · The Hacker News — npm 12 Disables Install Scripts by Default · Snyk · GitHub — Our plan for a more secure npm supply chain