• Agents
  • Pricing
  • Blog
Log in
Get started

Security for apps built with AI. Paste a URL, get a report, fix what matters.

Product

  • How it works
  • What we find
  • Pricing
  • Agents
  • MCP Server
  • CLI
  • GitHub Action

Resources

  • Guides
  • Blog
  • Docs
  • OWASP Top 10
  • Glossary
  • FAQ

Security

  • Supabase Security
  • Next.js Security
  • Lovable Security
  • Cursor Security
  • Bolt Security

Legal

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • Imprint
© 2026 Flowpatrol. All rights reserved.
Back to Blog

Apr 4, 2026 · 11 min read

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.

FFlowpatrol Team·Guides
npm Supply Chain Hygiene for Vibe Coders

You ran npm install. Did you check what you got?

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. Ship day. You moved on.

Here's what the AI didn't know: three weeks before you ran that install, chalk was compromised for two hours. So was debug. So were 16 other packages the entire JavaScript ecosystem depends on. 2.6 billion weekly downloads. Two hours. Every project that ran npm install in that window got the infected version. Automatically. No warning, no prompt, no way to know.

This happened on September 8, 2025. The attackers stole crypto wallet addresses from every compromised app's users. Then came the worm: the malicious code scanned infected machines for npm credentials and used them to publish infected versions of any package the victim maintained. Each compromise became a vector to infect downstream users. Self-propagating. First of its kind in npm history. The full breakdown is here.

That was the worst-case attack. This is how you make sure it doesn't happen to you.


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.

Diagram showing npm dependency chain with caret ranges auto-pulling new versions vs pinned versions staying locked
Diagram showing npm dependency chain with caret ranges auto-pulling new versions vs pinned versions staying locked

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.


Stop npm install. Start npm ci.

This is the single most important change you can make to your CI/CD pipeline. One command. No new cost. Eliminates the entire class of "install-time surprise" attacks.

npm install reads package.json, finds compatible versions, and is designed for flexibility. It can modify your lockfile if something doesn't match. Great for development. Terrible for production — it means every deploy can potentially pull a different (newer, compromised) version than your last one, even if your package.json hasn't changed.

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. Built for automated environments where "exactly what we tested" is not optional.

In your CI pipeline, replace this:

npm install

With this:

npm ci

That's it. One command swap. If you're using Yarn:

yarn install --frozen-lockfile

Same idea — enforce exact versions. The difference: npm install means "give me whatever npm thinks is compatible right now." That could be different from last week. npm ci means "give me exactly what's in the lockfile, or fail if you can't." When the Shai-Hulud attack published malicious versions of chalk on September 8, every project running npm ci was protected. Every project running npm install in that two-hour window pulled the infected version automatically.

Better yet, do both: check your lockfile into version control AND use npm ci in CI. Combined, they mean you only install versions your team has explicitly approved.


npm audit: catch what's already flagged

npm audit checks your dependency tree against a database of known compromised and vulnerable packages. It's not a crystal ball — it won't catch a brand-new attack in the two-hour window before npm's database updates — but it catches everything else. Automatically.

After you run npm ci, add npm audit:

npm ci
npm audit --audit-level=high

The --audit-level=high flag makes the build fail only on high or critical issues. Everything else is reported but doesn't block the deploy. You adjust the threshold based on your tolerance for risk.

In 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:

  1. Update the package to a patched version.
  2. Use npm audit fix if npm can automatically find a safe upgrade path.
  3. Override with npm audit --fix --force if you've evaluated the issue and determined it doesn't affect your code — but do this consciously, not reflexively.

The goal is to know what's flagged, not to make the check pass. Don't suppress the audit. Don't pretend the flag doesn't exist. Know your dependency risk before you ship.


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

HabitWhat it protects againstHow to do it
Commit your lockfilePrevents silent dependency drift between installsRemove package-lock.json from .gitignore
Run npm ci in CIEnforces exact lockfile versions in automated buildsReplace npm install with npm ci in CI/CD
npm audit --audit-level=high in CICatches known compromised packages before they shipAdd after npm ci in your pipeline
Pin direct dependenciesPrevents auto-install of new versions between lockfile refreshesRemove ^ from versions in package.json
Enable 2FA on npmBlocks publish-access phishing attacksnpmjs.com → Account Settings → 2FA
Remove stored npm tokens from .npmrcPrevents credential exfiltration if your machine is compromisedRun npm logout, check ~/.npmrc
Review lockfile diffs in PRsCatches unexpected transitive dependency additionsCheck the lockfile diff alongside package.json changes
Socket.dev on your repoBehavioral detection before advisory databases catch upInstall the GitHub app at socket.dev

Your five-step pre-deploy checklist

These habits take about 15 minutes to set up. Do them before you ship.

1. Commit your lockfile (2 minutes)

Check if package-lock.json or yarn.lock is in .gitignore. If it is, remove it. Then:

git add package-lock.json
git commit -m "commit lockfile"
git push

Every deploy should install from the committed lockfile. No exceptions.

2. Swap npm install → npm ci (1 minute)

Find everywhere you run npm install in an automated context — your CI config, your Dockerfile, your deploy script. Replace it:

# Before
npm install

# After
npm ci

One word. That's the entire change.

3. Add npm audit to CI (2 minutes)

In your GitHub Actions workflow, CircleCI config, or whatever CI you're using, add this right after npm ci:

npm ci
npm audit --audit-level=high

If it fails, fix before you ship. No overrides.

4. Pin your direct dependencies (3 minutes)

Open package.json. Look for any ^ before your direct dependencies. Remove them:

// Before
"dependencies": {
  "express": "^4.18.0",
  "axios": "^1.6.0"
}

// After
"dependencies": {
  "express": "4.18.2",
  "axios": "1.7.2"
}

Then run npm ci to update your lockfile. This prevents auto-installs of new (potentially compromised) versions between lockfile refreshes.

5. Scan your running app (5 minutes)

Run Flowpatrol against your deployed app. Paste your URL:

npm install -g @flowpatrol/cli

flowpatrol scan https://your-app.com

Or use the web dashboard at flowpatrol.ai. You're looking for exposed secrets, open API endpoints, misconfigured auth, and dependency issues that static analysis misses.

That's it. You just eliminated the entire install-time attack surface.


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.

Back to all posts

More in Guides

AI Agent Safety: What Your Agent Can Destroy (And How to Stop It)
Apr 3, 2026

AI Agent Safety: What Your Agent Can Destroy (And How to Stop It)

Read more
How to Secure Your MCP Setup
Apr 3, 2026

How to Secure Your MCP Setup

Read more
How to Secure Your Lovable App Before You Launch
Mar 28, 2026

How to Secure Your Lovable App Before You Launch

Read more