Vibe Coding, Security

Why npm audit fix Isn't Working (and How to Actually Fix the Vulnerabilities)

Why npm audit fix Isn't Working (and How to Actually Fix the Vulnerabilities)
If you ran npm audit fix, watched it churn, and got fixed 0 of 14 vulnerabilities — or worse, it "fixed" them and broke your build — you're not doing anything wrong. npm audit fix is conservative by design, and the vulnerabilities it leaves behind are almost always transitive dependencies: packages you never installed directly, pulled in three or four levels deep by something you did install.

This post explains exactly why the command stalls, why --force is a trap, and the three reliable ways to fix vulnerable dependencies without blowing up your app.

Join the Vibe Coding Community

Swap builds, trade feedback, and ship faster alongside other people vibe coding.

Join the Community

The One-Line Answer

npm audit fix only upgrades a package when it can do so without violating the semver range another package depends on. When the only available patch requires a major-version bump somewhere in the chain, npm refuses to touch it and reports it as unfixed.

To resolve those, you either upgrade the parent that pulls in the vulnerable package, or override the transitive dependency directly with an overrides block in package.json.

Why npm audit fix Reports "Fixed 0 Vulnerabilities"

Here's the mechanic. Your package.json lists direct dependencies with version ranges (^4.2.0, ~1.9.0). Each of those brings its own dependencies, which bring theirs, and so on. The vulnerability scanner walks that whole tree.

When a vulnerable package is buried in the tree, npm can only auto-fix it if a patched version exists that still satisfies every range pointing at it. If some-lib@^2 depends on vulnerable-pkg@^1.0.0, and the patch only lands in vulnerable-pkg@2.0.0, npm won't install it — that would break some-lib's declared contract. So it skips it and counts it as unresolved.

This is why the usual panic moves don't help:

Deleting node_modules and reinstalling rebuilds the exact same tree from your lockfile. Same versions, same vulnerabilities.

Deleting package-lock.json re-resolves ranges but lands on the same constrained versions, because the ranges in package.json haven't changed.

Running npm audit fix again does nothing new — it already did everything it was allowed to do on the first pass.

If you see a message like fixed 0 of 14 vulnerabilities or a list of issues marked as requiring manual review, you've hit the transitive-dependency wall. That's the signal to stop re-running the command and switch strategies.

Why --force Is Usually the Wrong Move

The tempting next step is npm audit fix --force. Don't reach for it reflexively.

--force tells npm to ignore semver and install whatever version clears the vulnerability — including major-version bumps on your direct dependencies. That means it can silently jump react-scripts from v4 to v5, or webpack from v4 to v5, introducing breaking changes you didn't ask for. Your audit might come back clean while your build is now broken, your config is incompatible, and you have no idea which of a dozen forced upgrades caused it.

There's a narrow case where --force is legitimate: when the only available remediation genuinely requires changing your root project's dependency ranges and you've accepted that. Even then, the rule is non-negotiable:

npm audit fix --force

npm run build      # must pass
npm test           # run the full suite

# then manually click through critical paths

If you can't commit to testing immediately after, don't run it.

The Three Fixes That Actually Work

1. Upgrade the Parent Dependency

The cleanest fix is to update the direct dependency that drags in the vulnerable package. Newer versions of the parent often already depend on a patched version.

Find what's pulling in the vulnerable package:

npm ls vulnerable-pkg

This prints the dependency path — the chain from your direct dependency down to the vulnerable one. Upgrade the package at the top of that chain:

npm install some-lib@latest

Then re-audit. If the parent's latest version moved to a safe transitive version, the warning disappears with zero risk to the rest of your tree.

2. Use overrides to Pin a Safe Transitive Version

When the parent hasn't shipped a fix yet — common with slow-moving or abandoned packages — you can force the resolution yourself. npm 8.3+ supports an overrides block in package.json (the equivalent of Yarn's resolutions):

{
  "overrides": {
    "vulnerable-pkg": "2.0.1"
  }
}

You can also scope an override so it only applies under a specific parent, which is safer:

{
  "overrides": {
    "some-lib": {
      "vulnerable-pkg": "2.0.1"
    }
  }
}

Then reinstall:

rm -rf node_modules package-lock.json
npm install
npm audit

This forces every copy of vulnerable-pkg (or just the ones under some-lib) to the patched version without waiting for the whole chain to catch up. Test afterward — you're overriding a version contract, so make sure the patched package is actually compatible.

3. Decide Which Findings You Can Safely Defer

Not every audit finding is a real risk to your app. A vulnerability in a package that only runs in a build script, or one that's only exploitable via an input path your app never exposes, may not be worth a risky upgrade. npm audit reports severity and the vulnerable path, but it doesn't know your runtime — a "high severity" prototype-pollution bug in a dev-only dependency is a very different thing from the same bug in your request-handling path.

The honest workflow is to read each advisory, check whether the vulnerable code is reachable in production, and prioritize accordingly — fix what's exploitable now, schedule the rest, and don't let a wall of dev-dependency warnings push you into a --force that breaks prod. For the bigger picture on which security work actually matters at each stage, see what every vibe coder needs to know about security.

A Repeatable Order of Operations

When npm audit fix stalls, run this sequence:

1. npm audit — read the actual advisories, severities, and paths. Don't just look at the count.

2. npm ls <pkg> for each vulnerable package — find the direct dependency responsible.

3. Try upgrading that parent (npm install <parent>@latest). Re-audit.

4. For anything still stuck, add a scoped overrides entry pinning the patched version. Reinstall and re-audit.

5. Only consider --force if remediation truly requires root-range changes — and gate it behind a full build + test run.

6. For findings that aren't reachable in production, document the decision and move on.

That's the whole game: stop fighting the auto-fixer, figure out who pulls in the vulnerable package, and fix it at the right level of the tree.

Vulnerable dependencies are only one of the risks AI-assembled apps inherit. Our guide to the best vibe coding security tools covers the other four — leaked secrets, injection, missing tests, and insecure defaults — with tools for each.

Find Every Vulnerable Dependency in Your Repo in One Pass

The manual npm ls / override loop works, but it's slow when you've got a dozen findings across a real codebase — and npm audit alone won't tell you which ones are actually reachable in your app versus buried in dev tooling.

Differ connects to your GitHub repo and runs a full security scan on first import — no live traffic, no setup. It surfaces vulnerable and outdated dependencies, tells you which are exploitable in your app's real paths, and proposes the specific upgrade or override for each one, so you're not running npm ls a dozen times by hand.

Connect your repo and get the free report in seconds — it'll show you exactly which of these vulnerabilities are worth fixing first. A clean dependency baseline is also step one toward an agent ready codebase — the repos AI coding agents can actually work in.