The Supply Chain Attack Nobody Tells You About: How I Learned npm ci Isn't Actually Clean
Admin User
Author
I was debugging a production incident last year when a colleague casually mentioned, "Yeah, we're running npm ci --ignore-scripts now. Should be safe." I nodded along, but something in that statement bothered me. We were treating a flag like it was a silver bullet—a get-out-of-jail-free card for dependency security. Turns out, I was wrong to be skeptical in the way I was. The real problem was much weirder.
A few months later, I came across the Shai-Hulud attack breakdown, and it completely rewired how I think about incident response in a Node.js project. This wasn't just another supply chain attack. This was something that exposed a blind spot in how we all approach security cleanup—a gap between what we think we're protecting and what we're actually protecting.
When ESLint Became a Trojan Horse
In August 2026 (or whenever this happened in the timeline), attackers compromised the npm account of a maintainer working on caching libraries. We're talking keyv, flat-cache, file-entry-cache—packages with 150+ million weekly downloads. The cascade was unavoidable: ESLint depends on file-entry-cache, which means if you were linting code, you were infected.
This is the kind of dependency graph that keeps me awake at night. You didn't install these packages directly. They came in through your linter. The malware, called Math_Symbol.js, used Bun (a trusted runtime) to steal credentials—npm tokens, AWS keys, GitHub PATs, Claude and Cursor IDE credentials. Then it self-replicated across 868 packages in hours.
The clever part? Sigstore signatures were valid. npm audit showed nothing. The traditional safety nets we've built failed completely.
The Persistence Layer Nobody Checks
Here's what actually disturbed me: the worm didn't just live in npm lifecycle scripts. It planted hooks directly into VS Code and Claude Code configuration files.
When you run npm ci --ignore-scripts, you're telling npm not to execute preinstall or postinstall hooks. It's a reasonable precaution. But VS Code doesn't care about that flag. It reads .vscode/tasks.json and executes any task with runOn: folderOpen the moment you open the folder. The worm knew this.
The same goes for Claude Code: it reads .claude/settings.json and fires SessionStart hooks whenever you start a session. If you're using Claude Code to investigate the attack, you've just handed the worm another execution window.
This is genuinely clever threat modeling. The attack surface isn't just npm—it's every tool that reads config from your repository.
My Take: We're Thinking About This Wrong
I've been in enough incident responses to know that the immediate cleanup workflow is: remove infected packages, delete node_modules, run npm ci --ignore-scripts, breathe. But Shai-Hulud forces a harder question: are we treating our IDEs as part of the application attack surface?
I think most of us aren't. We think of IDE config as harmless—it's just settings, right? Wrong. If a config file can be version-controlled, committed, and executed by an IDE, it's code. It should be audited like code.
The problem with automation here is real too. The worm used commit messages like "chore: update config" specifically to blend into noise. No automated tool will catch that. You need to manually inspect these files:
# Check VS Code tasks for anything with folderOpen
cat .vscode/tasks.json | python3 -m json.tool | grep -B2 -A5 "folderOpen"
# Check Claude Code hooks
cat .claude/settings.json | python3 -m json.tool | grep -A10 "SessionStart"
# Hunt through git history for config changes
git log --all --oneline -- "**/.vscode/tasks.json" "**/.claude/**"
This is tedious. It's the kind of thing we usually automate away. But that's exactly why attackers target it.
What I'd Do Differently
I'm adding IDE config inspection to our security checklist now. Not just for incident response—but as part of our normal dependency update process. Before we merge a PR that modifies dependencies, someone's looking at the .vscode and .claude directories too.
I'm also less comfortable with the assumption that --ignore-scripts is sufficient. It's still a good practice—I'm not abandoning it—but it's one line of defense, not the perimeter.
Your Move
If you're managing a team's development environments, the question isn't whether to trust your dependencies. It's whether you're trusting the entire configuration surface that those dependencies can touch.
Have you ever audited your .vscode/tasks.json in production? I'm guessing most people haven't.
Source: This post was inspired by "⚠️ Shai-Hulud opens your IDE: the npm worm that beats --ignore-scripts" by Dev.to. Read the original article