npm audit is a command that scans a project’s dependency tree for packages with known CVEs, reporting severity levels (LOW through CRITICAL) without distinguishing whether the vulnerable code can execute in a production runtime or is only present during local builds.
The number is real. The threat model is wrong.
npm audit gives you a severity rating. It does not give you a risk rating. Those are different things, and confusing them is where hours disappear.
Severity describes how dangerous a vulnerability is if exploited. Risk describes how likely exploitation is given your actual setup. A HIGH severity bug in a package that never runs in a user-facing environment has HIGH severity and near-zero risk. The audit tool reports the first. It has no mechanism to report the second.
The gap between severity and risk is widest at the build-time boundary.
What “build-time only” actually means
Most JavaScript and React Native projects have two classes of dependencies:
- Runtime dependencies — packages that ship into the final bundle or run on a server handling user requests. A vulnerability here is a live target.
- Build-time dependencies — packages that transform, bundle, or lint code during development. They execute on a developer’s machine or a CI runner. They produce an artifact. They are then done.
An attacker exploiting a vulnerability needs a path: malicious input reaches vulnerable code and produces a harmful outcome. For a build-time package, that path requires an attacker to already control your development machine or CI environment. At that point, the npm vulnerability is not the problem — a compromised machine is the problem, and it’s a categorically different incident.
The image-size / metro case, measured
Our own dependency tree, measured 2026-08-31, flagged two HIGH severity advisories on image-size. Both advisories had no first_patched_version — meaning no patch existed. Normally that would escalate triage priority sharply.
The lockfile told a different story. The parent pulling in image-size was metro — React Native’s bundler. Metro is a build-time tool. It processes assets during the bundle step and produces a packaged output. It does not ship into the runtime bundle that runs on a user’s device.
Risk path: zero. The two HIGH alerts required no developer action beyond documenting the triage decision.
This is not an edge case. It is the normal shape of a React Native audit. Metro and its dependency tree are large, and they live inside a project’s node_modules at full visibility to npm audit.
The React Native wrinkle that trips most teams
Here is the non-obvious part. The standard advice for suppressing build-time noise is npm audit --omit=dev — exclude devDependencies. That works for tools explicitly declared as devDependencies in package.json.
Metro in a React Native project is often listed as a regular dependency, not a dev dependency, because the React Native CLI requires it to be resolvable at that path. Running --omit=dev will not filter it out. The package is in the wrong bucket by convention, so the standard filter doesn’t apply.
This is why you need to trace the dependency chain, not just check the declaration type.
Three commands that actually move triage forward
1. Trace the parent chain before touching anything.
npm ls image-size
This prints every path in the tree that requires the flagged package. If every path leads to a bundler or a linter, you have a build-time vulnerability. If any path leads to your application code or a server package, triage as runtime.
2. Check whether a patch exists before spending time on workarounds.
npm audit --json | grep first_patched_version
A null or missing first_patched_version means the advisory has no upstream fix. Forcing a version bump through overrides may resolve the audit finding, but it may also silently break the parent package’s behavior. Verify before applying.
3. Use overrides surgically when a patch exists but the parent hasn’t updated.
In package.json:
json
"overrides": {
"image-size": "^1.1.0"
}
This pins the nested dependency to a patched version. It is not always safe — test the build — but it is the right tool when you need to satisfy a security scanner without waiting for the parent maintainer.
The decision rule
Route triage by runtime exposure, not by severity label.
| Dependency type | Exploitable path | Action |
|---|---|---|
| Ships in production bundle or server | Yes | Patch or isolate immediately |
| Build-time only, patch exists | No, but low cost to fix | Apply patch to clear audit noise |
| Build-time only, no patch exists | No | Document, suppress in audit config, revisit when patch ships |
| Build-time, mislabeled as regular dep (metro pattern) | No | Trace with npm ls, treat as build-time, note the label gap |
The fourth row is the one most triage guides omit. In React Native codebases it is also the most common row you will actually encounter.
What “no patch exists” tells you about severity ratings
When an advisory carries HIGH severity and lists no first_patched_version, it usually means one of two things: the maintainer has not released a fix yet, or the issue was reported against a use pattern that doesn’t apply to all consumers of the package.
Either way, the severity rating was assigned to the vulnerability in isolation, without knowledge of your dependency graph. The rating is not wrong — it describes the bug accurately. The rating is incomplete — it describes nothing about your actual exposure.
Audit tools are reporters. Risk assessment is still your job.
The measured image-size case is a clean example of that division of responsibility. The tool did its job correctly: it found two HIGH bugs and reported them. The human step — tracing metro as the parent, confirming build-time scope, confirming no patch — is what the tool cannot do and what no automation currently replaces.
Looking for more on ai & digital income? Visit SAVYX
Frequently Asked Questions
- Does npm audit distinguish between runtime and build-time dependencies?
- No. npm audit scans the full dependency tree and reports CVEs by severity without knowing whether a package executes in production or only during local builds. Tracing the parent chain with `npm ls` is the manual step that fills that gap.
- Why doesn’t –omit=dev filter out metro in a React Native project?
- Metro is often declared as a regular dependency in React Native projects, not a devDependency, because the CLI requires it to be resolvable at that path. The –omit=dev flag only suppresses packages explicitly listed under devDependencies in package.json.
- What should I do when npm audit shows HIGH severity but no first_patched_version?
- First, trace the parent chain with `npm ls` to determine runtime exposure. If the package is build-time only, document the triage decision and suppress the finding in your audit config. No patch means no forced upgrade path, so a workaround that breaks the parent package is not an improvement.
- Is it safe to use the overrides field to pin a nested dependency?
- Conditionally. If a patch version exists and the parent package has simply not updated yet, pinning via overrides often works. If no patch exists, an overrides entry may satisfy the scanner while silently breaking the behavior the parent package expects — test the full build before committing.
- How do I tell whether a flagged package ever reaches production?
- Run `npm ls
` and trace every path upward. If every parent is a bundler, linter, or test runner, the package is build-time only. If any path leads to your application source or a server-side package, treat it as a runtime exposure and prioritize accordingly.
Want to go deeper? Get our premium guides on SAVYX.
About the Author
The SAVYX Editorial Team researches and fact-checks practical guides on personal finance, AI tools, and productivity. Every article is reviewed for accuracy before publishing. Learn more about SAVYX or read our privacy policy.
Disclosure: As an affiliate, this post may earn a commission from qualifying purchases at no extra cost to you.

Leave a Reply