Every time you run npm install, you're not just downloading files. You're potentially executing arbitrary code, automatically, with your user permissions, before you've looked at a single line of what you just installed.
This isn't theoretical. Since the Shai-Hulud worm first appeared in September 2025, npm supply chain attacks abusing this exact mechanism have kept accelerating. On June 1, 2026, a campaign compromised at least 32 packages under the @redhat-cloud-services npm namespace, bypassing code review entirely through a compromised employee GitHub account.
How the hooks actually work
npm packages can define three install-time hooks in package.json:
- preinstall — runs before the package's dependencies are installed
- install — runs during the install step itself
- postinstall — runs after the package and its dependencies finish installing
{
"scripts": {
"postinstall": "node scripts/setup.js"
}
}
These exist for legit reasons: bcrypt and sharp compile native C++ bindings this way, Puppeteer downloads browser binaries. But npm runs these for every package in your tree, direct or transitive, by default. Only about 2.2% of packages on the registry actually define install scripts, so most of your tree doesn't need this at all. One compromised package in a tree of hundreds is enough.
Roughly how a real attack unfolds
A Microsoft-tracked campaign from May 2026 followed a pattern that's becoming common:
- Attacker registers npm packages under lookalike scopes mimicking internal corporate namespaces
- An inflated version number wins dependency resolution against your real internal package
- A
postinstallhook fires an obfuscated script that phones home and drops a payload, before you can intervene
That's the shape of it. The actual obfuscation technique and how the payload survives after the install process exits is where it gets interesting, and where most write-ups stop short.
Why this is genuinely hard to catch
- Legit packages need postinstall too, so blanket-flagging trains teams to ignore warnings
- Obfuscation makes a 17KB dropper look like nothing on casual review
- Cross-ecosystem placement: a May 2026 campaign hid malicious
postinstallinsidepackage.jsonfiles bundled in PHP/Composer packages, since PHP security reviewers don't think to check bundled JS tooling - The published npm tarball isn't guaranteed to match the GitHub repo. Attackers can publish a malicious tarball while keeping the public repo clean, and almost nobody diffs the two
npm audit and Dependabot are both reactive too. They rely on a CVE already existing. A freshly published malicious package with an obfuscated postinstall script sails through both cleanly.
The mental model worth keeping
npm install is not a download. It's potential code execution. Every dependency you add is code you're agreeing to run, not just import.
I go deep on this in the full write-up: the real obfuscation/C2 pattern from the May 2026 campaign, the monorepo and CI-specific exposure most guides skip, a copy-paste command checklist, and why --ignore-scripts breaks some legit packages (and the workaround for that).
🔗 Full guide: https://devencyclopedia.com/blog/npm-postinstall-attacks-2026
Also built a free tool if you'd rather get a risk-scored report than run manual greps: Lifecycle Hook Scanner
Top comments (0)