npm Supply Chain Hygiene for Vibe Coders
AI tools generate package.json with caret ranges that auto-install new versions. Here's how to lock down your dependency tree before a compromised package lands in your next deploy.
What your AI just put in your package.json
You described what you wanted to build. The AI wrote the code. Somewhere in that output was a package.json with a list of dependencies — express, chalk, axios, a date library, maybe a Stripe SDK. You ran npm install, everything worked, and you moved on.
Here's the thing: you probably didn't read that list. Neither did the AI, not in the way a human would. It suggested packages based on training data — code written before its knowledge cutoff. It doesn't know what happened to those packages last month or last week.
On September 8, 2025, attackers compromised debug, chalk, and 16 other packages covering 2.6 billion weekly downloads. The malicious code was live for two hours. Every project that pulled a fresh install in that window got the infected version automatically — no warning, no prompt. If you want the full story, we covered it in detail. The short version: a self-propagating worm stole crypto wallet addresses and exfiltrated npm credentials from anyone who installed during the window.
This isn't about that specific attack. It's about the habits that protect you from the next one.
The risk hiding in a single character
When an AI tool generates a package.json, it uses caret ranges by default:
{
"dependencies": {
"debug": "^4.3.4",
"chalk": "^5.3.0",
"axios": "^1.6.0"
}
}
That ^ before each version number means "this version or any compatible newer version." Run npm install today and you get the latest 4.x.x release of debug — whatever that is right now. Run it again in six months and you might get something different.
Caret ranges are npm's recommendation. They're in every tutorial. They're what the ecosystem expects. The AI isn't doing anything wrong. But they mean that if a package gets compromised after your lockfile was last generated, your next install picks it up automatically.
Pinned versions look like this:
{
"dependencies": {
"debug": "4.3.4",
"chalk": "5.3.0",
"axios": "1.6.0"
}
}
No caret. Exact version. npm install fetches precisely that release — nothing newer, nothing different. It's not exciting, but it means the thing you tested is the thing that ships.
The tradeoff is real: pinned versions mean you don't automatically get bug fixes and patches. The practical answer for most vibe-coded apps is to pin your direct dependencies and update them deliberately, on your schedule, rather than whenever someone runs npm install.
Your lockfile is a security artifact
Even with caret ranges, you have a powerful tool that most fast-moving projects ignore: the lockfile.
package-lock.json (npm) or yarn.lock (Yarn) records the exact version of every package in your dependency tree — not just your direct dependencies, but every transitive dependency those packages pull in. When you generate a lockfile and commit it, you've taken a snapshot of your entire dependency tree at that moment.
The lockfile only protects you if you use it correctly.
Commit it. Every time. package-lock.json and yarn.lock belong in version control. If they're in your .gitignore, remove them now. Without a committed lockfile, every fresh install is a fresh roll of the dice.
Review lockfile diffs. When a PR updates a dependency, the lockfile diff shows every package that changed — including transitive ones you've never heard of. This is tedious, but it's the only manual way to catch unexpected additions. A PR that bumps react from 18.2 to 18.3 shouldn't also be adding a new package that installs shell scripts.
Don't regenerate it casually. Deleting node_modules and running npm install from scratch regenerates your lockfile against current registry state. If anything was compromised in the interim, you've just imported it. Treat lockfile regeneration as a deliberate act, not maintenance.
npm ci vs npm install
This is the most impactful single change you can make to your CI/CD pipeline.
npm install reads package.json and installs compatible versions. If the lockfile exists, it tries to respect it, but it can modify the lockfile if something doesn't match. It's designed for development, where you want flexibility.
npm ci reads the lockfile and installs exactly what's there. It fails loudly if package.json and the lockfile are out of sync. It never modifies the lockfile. It's designed for automated environments where you want reproducibility.
# In your CI pipeline, your Dockerfile, your deploy script:
npm ci
# Not this:
npm install
If you're using Yarn:
yarn install --frozen-lockfile
One flag. That's the difference between "install whatever is compatible right now" and "install exactly what we tested."
npm audit in CI
npm audit checks your dependency tree against a database of known compromised and vulnerable packages. It's not perfect — it won't catch a brand-new attack that hasn't been flagged yet — but it catches everything that has been flagged, automatically.
Add this to your CI pipeline right after npm ci:
npm ci
npm audit --audit-level=high
The --audit-level=high flag makes the command exit with a non-zero status (failing the build) only when it finds high or critical issues. Lower severity findings are reported but don't block the deploy. Adjust the threshold based on your risk tolerance.
For GitHub Actions:
- name: Install dependencies
run: npm ci
- name: Security audit
run: npm audit --audit-level=high
If npm audit flags something, you have three options: update the package to a patched version, use npm audit fix if a safe fix exists, or add a specific override if you've evaluated the issue and determined it doesn't affect your use case. Don't ignore it and don't suppress the check — the whole point is to know.
Socket.dev: monitoring what npm audit misses
npm audit only knows about vulnerabilities after they've been reported and processed into the advisory database. The two-hour Shai-Hulud window happened before any advisory existed.
Socket.dev takes a different approach. Instead of checking against known bad versions, it monitors package behavior — flagging packages that newly start installing shell scripts, accessing new network addresses, or requesting filesystem permissions they didn't have before. It also monitors for typosquatting, dependency confusion attacks, and maintainer account changes.
It's free for public repositories. For private repos, there's a paid tier. The GitHub app installs in a few minutes and adds a check to every PR that touches your lockfile.
It won't catch everything. But it provides a layer of signal that sits upstream of the advisory database — behavioral analysis rather than signature matching.
If you maintain npm packages
The Shai-Hulud worm had a second payload beyond wallet theft: it scanned infected machines for stored credentials and used any npm tokens it found to publish malicious versions of packages the victim maintained. Each new victim became a vector for infecting their own downstream users.
If you publish to npm — even a small personal package — a few habits matter a lot.
Enable 2FA on your npm account. The initial Shai-Hulud attack succeeded through phishing. If the compromised maintainers had 2FA on publish actions, the attack fails even with stolen credentials. Go to npmjs.com → Account Settings → Two-Factor Authentication and enable it for both login and publishing.
Watch for .npmrc files. The worm specifically targeted .npmrc files, which often contain npm auth tokens in plaintext. Check ~/.npmrc and any project-level .npmrc files. If you have a token stored there that you don't actively need, remove it. Use npm logout to clear saved credentials when you're done with a publishing session.
Audit your published versions. If you were running npm install between September 8–9, 2025, and you maintain any packages, check whether any unexpected versions were published. Log into npmjs.com, navigate to your packages, and look at the version history. Anything you didn't publish is a confirmed compromise — unpublish it immediately and rotate your credentials.
Set up publish notifications. npm can send email notifications when a new version of your package is published. Enable this so you find out immediately if someone else publishes under your package name.
Responding to a supply chain incident
If you think you installed a compromised package, here's what to do.
Check your install logs against the incident window. For Shai-Hulud, the window was approximately 13:16–15:30 UTC on September 8, 2025. Look at your CI logs, deploy history, or local npm install timestamps. If you were in the window, assume exposure.
Rotate your npm token first. In npmjs.com → Access Tokens, revoke any existing tokens and generate fresh ones. This limits further damage from credential exfiltration even if it already happened.
Check your wallet-related code. If your app handles crypto transactions and was running in the install window, audit your transaction handling code for any address substitution logic that shouldn't be there.
Check your other stored credentials. The worm targeted anything it could find: GitHub tokens, AWS keys, .npmrc files, shell history. If your machine was potentially infected, treat all stored credentials as compromised and rotate them.
Pin and re-lock. After rotating credentials, update your package.json to pin exact versions and regenerate your lockfile from a clean state against the current registry.
Quick reference
| Habit | What it protects against | How to do it |
|---|---|---|
| Commit your lockfile | Prevents silent dependency drift between installs | Remove package-lock.json from .gitignore |
Run npm ci in CI | Enforces exact lockfile versions in automated builds | Replace npm install with npm ci in CI/CD |
npm audit --audit-level=high in CI | Catches known compromised packages before they ship | Add after npm ci in your pipeline |
| Pin direct dependencies |
What to do before your next deploy
These habits take about 20 minutes to set up. Most of them you only do once.
-
Check your lockfile. Is
package-lock.jsonoryarn.lockcommitted? If not, commit it now. If it's in.gitignore, remove it from there. -
Swap
npm installfornpm ciin every automated context — CI pipeline, Dockerfile, deploy script. One-word change. -
Add
npm audit --audit-level=highto your CI pipeline right after the install step. If it fails, fix before you ship. -
Enable 2FA on npmjs.com if you have an account with any published packages. Takes two minutes.
-
Install Socket.dev on your GitHub repo if you're open source or can afford the paid tier. Free for public repos.
-
Scan your running app with Flowpatrol to catch secrets, misconfigured headers, and dependency-sourced issues that static analysis doesn't see. Paste your URL, get a report.
The AI built your app fast. These six steps keep it solid.
The September 2025 npm supply chain attack was documented by Palo Alto Unit 42, Socket Security, and CISA's advisory "Widespread Supply Chain Compromise Impacting npm Ecosystem." For the full technical breakdown, see our Shai-Hulud case study.