The worms will continue until the ecosystem improves
458 packages. 5 ship to production. The rest are attack surface.
If you’ve ever been fishing, you’ve probably used earthworms (aka nightcrawlers) as bait for your lures. I didn’t know until recently that in North America, earthworms and similar species are actually invasive. In fact, for most of the last few millennia in the Midwest, worms rarely made an appearance due to climate conditions. Worms were introduced by settlers into the region. Since being introduced, the worms have completely changed the soil in forests. Without worms, fallen leaves decompose slowly, creating a thick, spongy layer of organic material called duff. This duff is vital for native wildflowers, ferns, and tree seedlings. Invasive earthworms devour this leaf litter at an unnatural speed, converting it into bare, compacted mineral soil.
To me, this seems relevant to what we’ve been seeing since last year with the Shai Hulud worms affecting NPM and other repos. I woke up on May 19, 2026 seeing yet another mass attack on NPM packages like we saw in months prior. These worms, instead of changing the soil characteristics of forests, have instead been wreaking havoc on organizations’ continuous integration (CI) pipelines and stealing credentials, secrets, and access.
In both cases, the worms have found a ripe ecosystem to overtake.
The reason why I’m seeing this as a fundamental problem with modern build systems is that we’ve built a massive infrastructure on open source tools and maintainers who have the best intentions but the most limited of time and resources to solve these challenges. On top of that, we see that not all build ecosystems are built the same.
When I look at the different build ecosystems, it’s generally some form of three-tier architecture: web frontend, API-based middle tier, and a database. The different layers are probably going to have a Node JS based frontend, a Node/Go/Python middle tier, and a database/datastore.
To identify the differences in ecosystems and to make this concrete, I built the same simple todo app — five routes, one table, three seed rows — across thirteen different stack combinations. Same functionality, dramatically different dependency footprints.
I built a React app frontend with Python, Node, and Go in the backend.
I built an HTMX app with Python, Node, and Go in the backend.
I even tried out a Templ frontend with a Go backend.
The numbers tell the story better than I can.
A React frontend backed by an Express API pulls in 458 npm packages before you write a single line of business logic. Switch the backend to Go: that number drops to 225 with the same React frontend and the same functionality. Drop React entirely for HTMX served by Go, and you’re at 2.
That’s not a rounding error. That’s a fundamentally different attack surface.
But total package count is only part of the picture. The more important question for supply chain security is: when do those packages actually execute? Most of them never ship to production. They exist only during the build, and the build is exactly where Shai Hulud lives.
In my case, 94% of the React frontend’s dependencies are build-time only. Libraries like Vite, esbuild, Rollup, and their plugin graphs don’t ship to production. Only 5 packages reach production. The other 77 exist solely to transform your source code into a bundle, and every one of them runs arbitrary code on your CI runner with full access to your environment, your secrets, and your npm publish token.
This is the duff layer. It built up slowly, package by package, as the ecosystem grew. And just like the earthworms converting forest floor into bare mineral soil, the Shai Hulud worms found it immediately useful.
I’ve had conversations with engineers over the years where build-chain vulnerabilities got downplayed because they weren’t exploitable in production. That reasoning made sense before supply chain attacks became a primary vector. It doesn’t hold anymore. These packages run arbitrary code on your CI runner with access to your secrets and your publish token. The fact that they don’t ship to production is exactly what makes them attractive to attackers. There’s no runtime detection, no WAF, no EDR watching the build pipeline.
On the one hand, a number of these build vulnerabilities are simply hard problems to solve, especially when it’s transitive through 2, 3, or 4 different layers. On the other hand, it’s becoming more apparent that a software supply chain is as weak as its weakest link, and we need to do better to vet these different dependencies.
You might think the answer is to choose a leaner framework. And you’d be partly right.
Swapping Express for Hono cuts the backend from 124 packages to 58. Dropping to bare node:http gets you to 56. These are meaningful reductions, but notice the floor. Every npm backend, regardless of framework, carries a ~19-package build chain that executes on every npm install. That’s not Express baggage. That’s npm. Native module compilation, prebuild installers, and the entire binary download machinery are there whether you want it or not.
Go doesn’t have this problem structurally. Go's full and runtime SBOMs are identical not because Go projects are carefully maintained, but because the module system makes it structurally impossible to include something you don't import. There's no install-time execution phase to exploit.
Python tells a similar story on the surface: FastAPI pulls in 79 packages in a full SBOM, with only 4 reaching runtime. But the mechanism is different. That inflation isn't a build toolchain like Vite. It's pip's own resolver and installer machinery, which means --ignore-scripts doesn't help you the same way. And as Mini Shai Hulud demonstrated, Python's attack vector bypassed install hooks entirely. Malicious code was injected directly into importable modules, executing at runtime on import, not at install time. PyPI's new release cooldown addresses the window of exposure, but it's a different worm entering through a different door.
What can you do to help solve the issue?
There are a number of measures organizations can take to tighten their build pipelines against various build threats. I don’t foresee the numbers of dependencies going down anytime soon, but reducing their impact can prevent the worms from infiltrating your software.
Reduce the surface before install even runs
--ignore-scripts/npm ci --ignore-scriptsin CI blocks pre/post-install execution for the whole graphConfigure
.npmrcwithignore-scripts=trueas a project default rather than relying on passing the flag in CI. This ensures the protection is source-controlled and applies to every developer's local install, not just the pipeline.minimumReleaseAgein package managers to prevent new versions of packages less than 3–7 days old from being used. Most worm campaigns tend to move fast and add changes rapidly to exploit build chains before detection. Renovate, Dependabot, npm, pnpm, pip, and uv all have these features.
Lock what executes in CI
Pin GitHub Actions to commit SHAs, not tags:
actions/checkout@v6is a mutable pointer,actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83ddis not. GitHub does allow for immutable releases to mitigate these threats but until you can verify the action's owner has opted in, the SHA is the only guarantee you control.Use
pull_requestnotpull_request_targetfor untrusted code. The pwn request vector TanStack got hit by recently is documented since 2021 and still common.Scope your OIDC publish tokens tightly: environment-scoped, short-lived, only on protected branches. A stolen OIDC token is how an attacker publishes malicious packages with valid provenance attestations: the signature is real because the token was real.
Know what you’re actually running
Separate full and runtime SBOMs: treating them as the same thing means you’re auditing a build compiler for runtime vulnerabilities, and the noise drowns out what actually matters in production. But don’t use “build-only” as a reason to ignore a vulnerability entirely. The Shai Hulud campaigns demonstrated exactly how build-time packages get weaponized; the distinction tells you where the risk lives, not whether it exists.
Strip observation tooling (cyclonedx, audit tools) from your dep counts and vuln scores because the noise hides the signal.
Watch the ecosystem signals
npm package provenance attestations are now widely available: prefer packages that publish them, treat absence as a yellow flag.
PyPI’s new release cooldown is a real improvement: Mini Shai Hulud crossing into PyPI suggests the worms are crossing into different ecosystems, slowly.
Where to go from here?
Earthworms aren’t leaving North American forests. The soil changed permanently the moment they arrived, and no amount of forest management reverses that. What changed is that ecologists now understand the mechanism: they know where the worms thrive, what they consume, and where the refugia are.
The same is happening here, slowly. The Shai Hulud campaigns are being documented, the attack vectors are being named, the tooling is improving. npm provenance exists now. PyPI has cooldowns. The GitHub Security Lab published the pwn request pattern in 2021. The knowledge is there.
But knowledge and adoption are different things. TanStack had signed provenance, 2FA on every maintainer account, and a documented CI vulnerability class that was four years old when they got hit. The worms don’t need a new attack. They just need the old ones to still work.
Harden your build chain. Understand what runs at install time versus build time versus runtime. Know the difference between your observation tooling and your actual dependencies. And then accept that the campaigns will continue, because the ecosystem that made them possible isn’t going away — we built it, we depend on it, and the maintainers keeping it alive are still doing it on nights and weekends with the best of intentions.
The worms will continue until the ecosystem improves. The ecosystem is improving. It’s just slower than the worms.
Terminology note: I use “full” and “runtime” SBOM as shorthand for all-installed vs. production-only dependencies. By CISA’s formal six-type taxonomy these map more precisely to Build and Deployed SBOMs. A true Runtime SBOM per CISA is generated by instrumenting a live running system to capture what’s loaded in memory, which is a stronger guarantee than what’s shown here.






