• Agents
  • Docs
  • 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

  • Blog
  • Docs
  • FAQ
  • Glossary

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
Guides

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.

Flowpatrol TeamApr 4, 202610 min read
npm Supply Chain Hygiene for Vibe Coders

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.

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.


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

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 dependencies

What to do before your next deploy

These habits take about 20 minutes to set up. Most of them you only do once.

  1. Check your lockfile. Is package-lock.json or yarn.lock committed? If not, commit it now. If it's in .gitignore, remove it from there.

  2. Swap npm install for npm ci in every automated context — CI pipeline, Dockerfile, deploy script. One-word change.

  3. Add npm audit --audit-level=high to your CI pipeline right after the install step. If it fails, fix before you ship.

  4. Enable 2FA on npmjs.com if you have an account with any published packages. Takes two minutes.

  5. Install Socket.dev on your GitHub repo if you're open source or can afford the paid tier. Free for public repos.

  6. 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.

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
Prevents auto-install of new versions between lockfile refreshes
Remove ^ 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