npm just shipped the biggest change to its install behavior in sixteen years. As of version 12, running npm install no longer executes a dependency's preinstall, install, postinstall, or prepare scripts unless you have explicitly allowed that package. That one default closes the delivery mechanism behind most of the past year of npm supply-chain attacks, including the 500-package install-script wave in June.
In the same week the new default landed, two live compromises hit the registry. One of them opened with exactly the install script npm v12 blocks, then moved its payload somewhere the block does not reach. The other never touched an install script at all. Both stole credentials from anyone who pulled the package. If you are reading npm v12 as the end of dependency risk, these two incidents are the correction, and they are worth walking through because they show which controls still carry weight after the install-script era closes.
Who this is for, and who can skip it
This matters to any team whose builds or developer laptops pull from npm: product engineering, SaaS startups, agencies, and the one-person shop whose machine runs npm install a dozen times a day and also holds AWS keys, a live cloud CLI session, and a browser logged into everything. The infostealers in both incidents this week went straight for that machine. They read wallet files, cloud credentials, CI tokens, browser data, and the config files of AI coding assistants like Claude and Cursor, then shipped them off the box.
If you run no JavaScript and have no Node toolchain on any developer or build host, this specific pair is not yours. Keep the mental model anyway. The same install-script lockdown is already being discussed for other ecosystems, and the two paths that beat it here - code that runs when you import a package, and a malicious release published through a trusted account - exist in every package manager. The registry changed a default this week; the attackers changed a technique. Knowing which is which tells you where to put your time.
What npm v12 actually turns off
The headline change is script execution. Lifecycle scripts that used to run automatically on install are off by default. To let a package that legitimately needs to compile a native module run its script, you run npm approve-scripts, which writes an allowlist into package.json that you commit alongside the lockfile. Two related defaults tightened at the same time: --allow-git now defaults to none, so Git-hosted dependencies do not resolve unless you opt in, and --allow-remote defaults to none, which closes the remote-tarball path. All three have been available behind warnings since npm 11.16.0, so you can see what breaks in your tree before you take the upgrade; GitHub laid out the full set of breaking changes when it pre-announced v12.
This is a genuine reduction in attack surface. For most of the past year, the fastest way to get code running on a developer's machine was to publish a package whose postinstall hook fired the moment it was pulled into a dependency tree - no import required, no application code touched, no user interaction. Turning that off by default is the right move, and you should not wait to be forced onto v12 to get the benefit. Before you upgrade, it helps to know which of your existing dependencies actually declare an install script, because those are the packages you will be prompted to allowlist. This one-liner enumerates them:
# List installed packages that declare a lifecycle install script.
# These are exactly what npm v12 will block until you approve them.
find node_modules -maxdepth 2 -name package.json -exec node -e '
const p = require(process.argv[1]);
const s = p.scripts || {};
const hooks = Object.keys(s).filter(k => /^(pre|post)?install$/.test(k) || k === "prepare");
if (hooks.length) console.log(`${p.name}@${p.version}: ${hooks.join(", ")}`);
' {} \; 2>/dev/null | sort -u
Run that today and you get your allowlist candidates. The question npm v12 does not answer is what an attacker does the day after install-script blocking becomes the default. This week answered it twice.
jscrambler: the payload that stopped needing the script
On July 11, an attacker published five malicious versions of the widely used jscrambler package - 8.14.0, 8.16.0, 8.17.0, 8.18.0, and 8.20.0 - over roughly three hours, interleaved with the maintainers' attempts to pull them. The first versions used a classic install hook: "preinstall": "node dist/setup.js", the exact construct npm v12 now blocks. Then, starting with 8.18.0, the attacker moved the dropper out of the install script and into the package's own runtime code, in dist/index.js and dist/bin/jscrambler.js, written as functions that execute themselves the moment the module loads.
That relocation is the whole lesson. A dependency you import is code you have agreed to run. When the malicious logic lives in the module body instead of a lifecycle hook, it fires the first time your application, your test suite, or your bundler requires the package. Install-script blocking never sees it, because nothing about it is an install script. In jscrambler's case the loader decompressed a Rust-built infostealer tailored to the victim's operating system and went after crypto wallets, browser data, Slack and Discord tokens, cloud credentials from the AWS, GCP, and Azure metadata paths, and the config files for several AI coding assistants, then set up persistence through systemd and cron. Socket flagged the first bad version six minutes after it went up. The last known-clean release is 8.13.0, and 8.22.0 is confirmed safe.
Injective: the release that was trusted
The second incident removes the install script from the story entirely. On July 8, @injectivelabs/sdk-ts@1.20.21 shipped with malicious code woven into legitimate key-derivation functions. A call to trackKeyDerivation(), dressed up as anonymized usage telemetry, batched private keys as they were generated and posted them to an attacker-controlled host over HTTPS. Seventeen other @injectivelabs packages pinned that exact SDK version, so every downstream project that depended on them inherited the compromise transitively, without ever naming the poisoned package in its own manifest.
What makes this one worth studying is how the release was published. The malicious commits were pushed under the identity of an existing, trusted maintainer and shipped through the repository's own OIDC trusted-publisher pipeline. It was a signed release, carrying provenance, published by the legitimate automation. Nothing about install-time behavior was involved, and nothing about the delivery looked abnormal to a tool checking that a package came from where it claims. When an attacker controls a maintainer's account, the release they ship is trusted because it genuinely is - the trust is real and aimed at the wrong hands. Provenance tells you a package came from the expected pipeline. It does not tell you the person driving that pipeline is who you think.
The decisions npm v12 leaves you
Two surviving paths, two different sets of controls. Neither is exotic, and both are things a small team can put in place this week. Here is where the effort pays back.
1. Turn install-script blocking on now
You do not need to wait for the forced v12 upgrade. Set ignore-scripts=true in a project or user .npmrc, run installs with npm ci --ignore-scripts in CI, and build your allowlist of the handful of packages that legitimately need to compile. This closes the shrinking-but-real install-hook path, which was still live in jscrambler's first three versions.
2. Pin, lock, and quarantine new releases
Pin exact versions, commit the lockfile, and put a cooldown between a release appearing and your build adopting it. jscrambler's malicious versions lived for hours; Socket caught the first in six minutes. A one- or two-day quarantine on brand-new versions turns that detection window into your protection for free.
3. Verify provenance, then keep going
Run npm audit signatures to confirm your dependencies carry registry signatures and provenance attestations. Treat a clean result as necessary and not sufficient: Injective had a legitimate, signed, provenance-carrying pipeline, and it still shipped a wallet stealer. Provenance raises the floor; it is not the ceiling.
4. Shrink what a developer machine can lose
Both stealers monetized the same thing - a workstation that runs installs and also holds long-lived secrets. Get standing cloud credentials, production keys, and unscoped tokens off the machines that pull packages. Short-lived credentials, per-build identities in CI, and separate profiles for high-value access mean a compromised install costs an attacker far less.
5. Treat import as execution
The jscrambler payload proves the assumption you have to make: any dependency can run code the instant it loads. Run installs and first builds of untrusted or newly bumped dependencies with no ambient network access, and monitor egress from build agents so an unexpected outbound POST from your CI runner becomes an alert instead of a footnote.
# Controls you can apply today, before the forced v12 upgrade.
# 1. Block lifecycle scripts for every install in this project.
echo 'ignore-scripts=true' >> .npmrc
# 2. Deterministic, script-free installs in CI.
npm ci --ignore-scripts
# 3. Confirm registry signatures and provenance on the resolved tree.
npm audit signatures
# 4. Refuse the known-bad jscrambler releases; pin to the last clean one.
npm install jscrambler@8.13.0 --ignore-scripts --save-exact
Block the scripts now, then close the paths npm v12 leaves open
Enable install-script blocking today rather than waiting for the upgrade to impose it, and commit the allowlist so your team inherits the decision. Then spend the rest of the week on the paths this week's incidents actually used: pin and quarantine new versions so a three-hour attack window closes before your build reaches it, verify provenance without trusting it blindly, and take the standing secrets off the machines that run npm install. npm v12 shut one door for you. The value now is in guarding the two it left open. If you want a second set of eyes on your dependency policy, CI trust model, or the blast radius of a compromised package, that is the kind of review we do.
Need a second set of eyes on your build pipeline?
We review software supply chains - dependency policy, CI/CD trust, and the blast radius of a compromised package - for teams that ship on npm and other registries. Book a session to walk through yours.
