- Published on
Why npm supply-chain attacks won't go away — what npm-scan actually checks, and the defenses that work
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- Introduction — npm supply-chain attacks are structural, not occasional
- Why npm is structurally exposed
- What npm-scan actually checks (and what it can't verify)
- The defenses that actually work
- Closing
- References
Introduction — npm supply-chain attacks are structural, not occasional
News of an npm supply-chain compromise barely registers as news anymore; it is closer to background noise. A popular package gets hijacked, a token leaks from a plain install, and a few days later the same thing happens in another package. The reason this is not a run of bad luck is that npm's design hands attackers a wide surface by construction. Using npm-scan — a tool that trended recently on Hacker News — as a way in, this piece looks honestly at where that surface actually is, and where a tool can and cannot close it.
One thing up front. The npm-scan README is confident and leads with numbers that are hard to verify. I think the problem it targets is real; I do not take its numbers at face value. So let us separate the problem from the tool.
Why npm is structurally exposed
Three things overlap. First, install scripts. An npm package can run arbitrary code in its preinstall, install, and postinstall hooks, and that code runs with the privileges of whoever ran the install. npm install is, quite literally, untrusted code execution. Second, transitive dependencies: you may pick ten packages directly, but node_modules ends up holding hundreds, and you have already signed up to trust all of those maintainers. Third, typosquatting — names one keystroke apart, like lodash and lodahs, turn a single typo into a malicious install.
Individually each of these is manageable; the danger is that they compose. A typosquatted name pulls in a package, its postinstall runs on install, and because the thing arrived as a transitive dependency of something you trusted, nobody ever reviewed it. The three weaknesses are less three separate risks than one pipeline.
The 2025 Shai-Hulud worm showed this is not theory. Per Palo Alto Unit42's analysis, the worm executed via install scripts, authenticated to the registry with a stolen npm token, and self-replicated by injecting malicious code into other packages maintained by the same developer. Its targets were npm tokens in .npmrc, GitHub PATs, and API keys for AWS, GCP, and Azure. The November "Shai-Hulud 2.0" moved execution from postinstall to preinstall, which, in Unit42's words, "completely eliminates the need for human interaction, guaranteeing execution on virtually every build server." That 2.0 wave produced over 25,000 malicious repositories across roughly 350 users, spanning tens of thousands of GitHub repositories.
Underneath all three is a fourth factor: the maintainer account. Most of these campaigns do not begin by finding a bug in your code — they begin by taking over a trusted publisher, through a phished login or a leaked token, and pushing a malicious version of a package you already depend on. Once a legitimate account publishes it, every mechanism above carries it to you automatically. That is why the supply chain, not any single package, is the real unit of risk.
The key point is that none of this needed an exotic zero-day. It used features npm ships on purpose — install scripts and publish rights. That is why it does not go away.
What npm-scan actually checks (and what it can't verify)
npm-scan is a mostly-JavaScript CLI distributed as @lateos/npm-scan. Its slogan is that it "catches what npm audit, Snyk, and Socket miss." Usage is simple:
npm install -g @lateos/npm-scan
npm-scan axios # scan a single package
npm-scan scan-lockfile # scan the whole lockfile
npm-scan express --json > findings.json
What the README advertises is static and behavioral analysis across "23 detectors (D1–D25)." It runs entirely locally with no telemetry or cloud dependency, and targets under 30 seconds per CI run. The output side is genuinely practical: SARIF for GitHub Security, CycloneDX and SPDX SBOM export, YAML policy allowlists, and GitHub Actions integration. The threat categories it names line up neatly with the section above — credential theft, obfuscation and anti-debugging, extraction of OIDC and AI keys from memory, and worm propagation.
So far, reasonable. The problem is that the README pins detection rates like 98% and 99% to each category. There is no published methodology, dataset, or benchmark behind those numbers. If you cannot see what was measured, against what sample, and what counted as a true positive, that is a claim, not a measurement. The README also says nothing about false positives or limitations — and every scanner has both. A security tool that advertises only strengths is, by itself, a reason for caution. Nor could I see what the individual detectors D1–D25 flag: the linked detail file did not open when I checked.
Finally, licensing. It is free under MIT for individuals, open source, and evaluation, but production use requires a commercial license running from $799 to $9,900 per year. Worth knowing before you wire it permanently into CI.
One more note on the framing. "Catches what npm audit, Snyk, and Socket miss" is a competitive claim, not a neutral one — Socket and Snyk are mature tools with their own behavioral analysis, and the word "miss" is carrying a lot of weight. Treat any "we catch what the incumbents don't" pitch as a hypothesis to test against your own dependencies, not as a finding.
The defenses that actually work
A scanner is one layer, and a determined attacker routes around heuristics. What actually shrinks the blast radius is the boring fundamentals.
- Turn off install scripts. Setting
ignore-scripts=truein.npmrc, or usingnpm install --ignore-scripts, removes the very execution point the Shai-Hulud family relies on. It is not free — packages with genuine build steps, like native modules, need an exception — but most dependencies need no scripts at all. - Lockfiles and
npm ci. Commitpackage-lock.jsonand usenpm ci, notnpm install, in CI: it installs the pinned tree exactly and fails on drift. Make reading the lockfile diff in a PR a habit, and an unexpected new transitive dependency becomes visible. - Verify provenance. npm provenance and trusted publishing tie a package to the CI that built it;
npm audit signatureschecks the attestations. - Fewer dependencies. The most fundamental of all. Every transitive dependency is attack surface and one more maintainer to trust. Before adding a package for a one-line utility, ask whether it is worth the surface.
- Manage tokens. Do not leave
.npmrctokens sitting on build machines; scope them narrowly and rotate them. Remember that Shai-Hulud went looking for exactly that file.
These are in rough order of leverage. Disabling install scripts comes first because it shuts the exact door the recent worms walked through; fewer dependencies comes last only because it is the slowest to act on, not the least important — over time it is the change that shrinks everything else.
Scanners like npm-scan, Socket, and Snyk are worth having as a layer on top of these fundamentals. They do not replace the list.
Closing
npm supply-chain attacks persist because they are misuse of features, not bugs. Install scripts and publish rights are the very things that make npm convenient, and attackers use them as intended. So this is not a problem that gets cured; it gets managed.
None of this is cause for fatalism. The trajectory is worsening — pre-install execution and self-replicating worms are a real escalation — but the countermeasures are well understood and mostly free. The gap is not knowledge; it is that the boring hygiene rarely gets prioritized until after an incident.
The same posture is the right one for a tool like npm-scan. The problem it targets is real, and design choices like local execution, SARIF, and SBOM output are sensible. But detection rates with no methodology, and silence about limitations, ask for verification rather than trust. Keep the scanner as one layer, and do not forget that what actually shrinks the blast radius is lockfiles, disabled scripts, provenance, and fewer dependencies. That is the honest conclusion.