11 minute read

How to protect yourself from npm worms like Shai-Hulud

After watching Shai-Hulud spread through npm in a matter of minutes, I audited my entire dev workflow. Here's what I'm doing differently to keep my machine safe.

Not too long ago, I wrote about my dev command and why I did it in the first place: trust nothing and isolate everything.

This time, I’m updating it so that I’m even more protected in the future.

npm is under siege

For the past 9 months, npm has been attacked relentlessly. At the time of writing, there have been at least 6 distinct attack campaigns targeting the registry—and that’s counting only the major ones.

Since January 2026, over 16,000 packages have included malicious code (source). Security researchers at OX Security identified over 2,100 GitHub repositories hosting stolen credentials from the latest Shai-Hulud wave (source). These are no small figures.


It’s interesting that these campaigns seem to target developers specifically. It’s as if the modern gold are the tokens and API secrets.


The way these attacks happen is through something called post install scripts—a script that will run once a certain package is installed.

The main issue here is trust.

You trust these packages are safe. You trust they come from a safe provider. You trust the post install script is safe to run too. But we can’t trust anything nowadays.

npm itself seems to be a low barrier way to distributing packaged code. In other words, anyone can create an account and publish a package with no review or vetting. You just run npm publish and it’s live for anyone to pick.

But we can’t blame npm here, because it’s one of the biggest, if not the biggest, package registry right now with only around 10 years of age. That’s remarkable! And being open has benefits too, not just downsides. That’s one of the reasons why it grew so much and so fast.

Shai-Hulud: what is it?

Put simply: a worm.
It’s a self-contained malware that’s able to self-propagate too. One of the reasons why it has spread so fast.

Unlike a virus, which needs a host to survive, a worm lives on its own and does what it needs to survive.

Shai-Hulud specifically was designed to fetch very specific things: npm tokens, GitHub personal access tokens, AWS keys, cloud credentials, SSH keys, vault tokens, database credentials, CI/CD account tokens… You get the point.

It finds a way to learn just about anything you work on and impersonates you by using your own secret tokens.

As soon as a developer installs an infected package with the usual npm install command, the hunting for these tokens begins as a post install script. See the trust issue?

After fetching these precious tokens, they are sent to remote servers where we can only assume they get collected. But not blindly! They are encrypted before sent over the network, so the chances of getting caught are even slimmer. Smart!

But the absolute genius part about this—because let’s face it, even though this is malicious we should still recognise its genius!—is that if the user who just ran npm install has the ability to publish npm packages themselves, the post install script adds the worm directly to the packages the user can publish, bumps the version number and then publishes them automatically, as legitimate new versions of trusted packages.
This is how it self propagates across the registry.

Absolute genius! There’s no other way to put it…

Actually, here’s one more for the genius board:
When the worm infects a machine, it deploys a persistent system daemon called gh-token-monitor.

The purpose? Let’s unlock a new fear, shall we?

The sole purpose for this daemon is to poll the GitHub API every 60s, checking if a specific token is still valid. If it got revoked, it runs rm -rf ~/—immediately wiping the user’s entire home directory.

Why is this genius? Because the main thing you want to do when you fear you’re infected is preventing any more damage. In this case, you do that by revoking your tokens, which basically removes the access the worm had. But if you do that, your entire home folder vanishes.

So you are put between a rock and a hard place debating whether you prefer to format your computer or wait and see what the worm will do with your credentials.

Protecting yourself against these attacks

So what can you do to protect yourself? Well, one thing most of us can do and will likely never notice anything is disabling these post install scripts—the main way the worm gets what it wants.

You do this by adding ignore-scripts=true to your .npmrc file.

This also means that if you attempt to install a script that needs a post install script, it will likely fail and you will need to run the command yourself manually. But hey! at least you can inspect what it’s attempting to run before it actually runs it.

That’s a win in my books!

Minimum age required

Just like you couldn’t enter pubs at 13 years old, packages should also be age-checked before entering your system.

You do this by adding min-release-age=7 to your .npmrc file.

This way, npm will only attempt to install packages that are older than 7 days. Can you imagine how much slower the worm would be moving if most developers had this requirement in their systems? I’d guess a lot slower!

Put together, your .npmrc should look like this:

Plain text
.npmrc
12
ignore-scripts=true
min-release-age=7

Drop that file in your home directory and you’re covered.

Isolate everything

Now, the reason why I mentioned my dev command above is because I still believe it’s the best way to have peace of mind while still developing and contributing to third party repositories.

Think about this: if everything you run in your computer runs in an isolated box, without being able to reach outside of the box, the chances of malicious code to do any harm are far slimmer, because all they find is mainly a bare bones system. Mainly…

Your tokens are likely still compromised, but that depends on how you want to expose them to your containers.

If you include a .env file in your repo and then mount the whole repo folder as a volume in your container, the container has access to the contents of the .env file. So the worm will too.

So both suggestions from above to .npmrc file still apply, even with containers.

But at least, the code is running in isolation. It doesn’t have access to your home folder, your other projects, your browser cookies, your npm configs and tokens, etc.

DNS based protection

The whole point of the worm is to fetch users’s credentials and then send them elsewhere. And there’s another way you can stop this from happening: point every domain name used by the worm to your localhost. This is called a sinkhole.

This will basically make it so that the worm acts as if it’s offline, because it won’t be able to reach for the servers it was designed to.

That said, sinkholing is not a silver bullet. If your machine is infected, the worm can still read your tokens locally, still publish packages in your name (if you’re a maintainer), and still run the home-dir wipe if you revoke the token.

What sinkholing stops is the attacker receiving your stolen data—the credentials never leave your network.

So even if you’re breached, the attacker walks away empty-handed. They can’t collect what they came for.

Putting it all together

As these attacks happen more frequently, I set up to audit my own dev command in search of places I could harden it even more. And found plenty:

If you’re curious and want to check this dev command, or actually want to use it for yourself, it’s open and you can check it at pmpinto/dev-command.

Be careful with over privileged AI agents

One last thing…

For the past few months I have personally been exploring opencode as a way to improve my knowledge, speed and ability to do things that would either take me much longer to do by hand or just outright do things I wouldn’t even be able to do on my own.

Opencode differs from your common AI chat bot in a very specific and powerful way: it runs directly in your system and has access to your files.

With great power comes great responsibility.

As you can imagine this can backfire quite rapidly unless you tame the beast. And even though you can write your own skills so that it does things in a very specific way, for things like permissions, you want a config file, not a skill.

Luckily for us, opencode supports this.

I have set mine to never run things like npm, npx, node or even docker commands. Never attempt things like rm -rf / and never read secrets, credentials, environment files or ssh sensitive information. Couple with a skill, it should prompt me to do it rather than doing it.

For instance, if I’m updating a component on a website it might attempt to run npm run test to make sure everything is still passing. But I don’t want that, because that would be running on my host machine.

This way I’m sure it won’t just be installing npm scripts on my machine, completely bypassing my containerized approach in the first place.

Takeaways

A few months ago I said that your machine is not a dev environment. This audit proved me right.

The same isolation-first approach that keeps my system clean also protects me against worms like Shai-Hulud. The only difference is now the line of defense is even thicker: npm hardening, sinkholing, capability dropping, and AI restrictions.

I can tell you that I sleep better knowing my tokens aren’t one npm install away from leaving my network. But I can also tell you I can’t wait to retire!

Photo of Pedro