The first time I ran npm audit against a fresh monorepo, it printed seventy-three high-severity advisories and then suggested I run npm audit fix --force. The command, cheerfully, wanted to downgrade our primary framework by two major versions. I closed the terminal, opened it again, and asked a question that has nagged me ever since: what is an audit tool actually doing, and why do the three popular ones disagree so often?
Over the last year I've pushed the same lockfiles through npm 10.2, pnpm 8.15, and Yarn 4.1. The outputs rarely agree. Sometimes the disagreement is a missing advisory. Sometimes it is a phantom one. Sometimes the three tools agree on the advisory but disagree on whether it is reachable from your app. If you're a platform engineer trying to standardize a DevSecOps pipeline across repos, that variance matters. This post is my attempt to lay out, honestly, what each audit command does well and where each one quietly fails.
The Shape Of An Advisory Lookup
All three tools begin at the same place: a dependency tree derived from the lockfile. From there, they query an advisory database, match package names and semver ranges, and print a report. The differences live in what database they query, how they resolve transitive paths, and how they classify "severity".
npm audit was first. Introduced with npm 6 in May 2018, it ships with the default CLI and talks to the GitHub-operated npm Advisory API (the endpoint you can still hit at registry.npmjs.org/-/npm/v1/security/advisories/bulk). Since the GitHub acquisition, that feed is a mirror of the GitHub Advisory Database, which means it carries both npm advisories and general-purpose advisories with npm ecosystem tags. It is comprehensive, but occasionally noisy: advisories for packages that you consume only at build time are reported at the same severity as advisories in your production bundle.
yarn audit arrived in Yarn 1.12 and originally consumed the same npm endpoint. With Yarn 2 and the Berry rewrite, Yarn shifted to a pluggable resolver. Today, with Yarn 4, yarn npm audit calls the npm endpoint and layers in a slightly stricter semver parser. The difference I've seen most often is how Yarn handles packages published to multiple scopes or with aliased names: Yarn tends to report fewer false positives on alias-based installs than npm's default audit does.
pnpm audit, added in pnpm 4 back in 2019, also queries the npm endpoint by default, but pnpm's content-addressable store means its dependency tree is more precise. When a transitive dependency is deduplicated at one version for one workspace and a different version for another, pnpm reports both paths separately. npm and Yarn often collapse those.
Severity: The Word That Means Nothing
A "critical" advisory in the GitHub Advisory Database is derived from CVSS v3 scores plus a manual GitHub review. npm, Yarn, and pnpm all bucket advisories into info, low, moderate, high, and critical. None of them, out of the box, compute reachability. If a critical RCE lives in a library you import only inside a Jest test, it still prints red.
Yarn 4 has a useful yarn npm audit --recursive --severity moderate flag that will filter by severity and recurse workspaces. pnpm's equivalent is pnpm audit --audit-level moderate --prod, which I lean on heavily because the --prod flag drops devDependencies from the report. npm added --omit=dev in npm 8.3, which does the same thing, though I find the pnpm flag easier to remember.
None of these flags make a critical CVSS score meaningful to your application. That is still a human judgement, and it is where I spend most of my triage hours.
Known Divergences In The Wild
A concrete case from March 2024: CVE-2024-28849, the follow-redirects credential leak. All three tools reported it against follow-redirects@<1.15.6. npm reported 414 unique paths in our monorepo because it walked each workspace independently and did not deduplicate the display. pnpm reported 18 paths, one per true dedup bucket. Yarn 4 reported 22 paths, and the extra four were due to a build-time mirror that npm had collapsed into production. The underlying vulnerability count was identical. The engineer reading the report would believe three different things.
Another case: CVE-2023-26136, the tough-cookie prototype pollution in June 2023. npm audit flagged it with severity moderate. yarn npm audit flagged it as moderate as well. pnpm audit pulled the OSV.dev severity when running with pnpm audit --json --fix and briefly classified it high before pnpm 8.7 aligned its severity mapping back to the GitHub Advisory Database default. That kind of drift is rarely announced in release notes; you find it when a dashboard flips color overnight.
Transitive Reach And The overrides Question
When an advisory applies to a transitive dependency, fixing it requires either a direct update of the parent (which may not exist) or a manual override. npm introduced overrides in npm 8.3, Yarn has resolutions, pnpm has pnpm.overrides. All three do roughly the same thing. The audit tools handle them differently: npm audit will re-check after an override is applied and remove the advisory; yarn npm audit does the same; pnpm audit has, in my testing, occasionally left a stale advisory in the output when the override pinned a version outside the vulnerable range but inside a weird prerelease band. Deleting node_modules and reinstalling clears it, but I've filed this twice.
CI Flags That Matter
For a CI gate, the three relevant flags are --audit-level, --production (or --prod / --omit=dev), and a JSON output format. I default to:
- npm:
npm audit --audit-level=high --omit=dev --json - yarn:
yarn npm audit --severity high --environment production --json - pnpm:
pnpm audit --audit-level=high --prod --json
All three return non-zero on findings, which is what you want for a pipeline. None of them return a useful SARIF file natively; you'll need a converter. Microsoft's npm-audit-sarif still works for npm output. For pnpm and Yarn, I maintain a small jq transform because nothing on the market handles them cleanly.
What I Actually Run
In my own repos, I run pnpm audit --prod as the primary gate because the deduplication is honest and the output is quiet. I run npm audit --omit=dev as a secondary gate for anything still on npm 10. I only reach for yarn npm audit inside Yarn 4 projects; the Berry resolver is strict enough that the tool is credible, but legacy Yarn 1 audit results I treat as advisory-only. None of these replaces a real SCA scan with reachability analysis and a VEX workflow; they are the first filter, not the last one.
How Safeguard Helps
Safeguard normalizes advisory data from GitHub's feed, OSV.dev, and vendor-specific sources so your audit signal does not depend on which package manager you happened to run. Our reachability engine walks the dependency graph and marks findings as reachable or dormant, so a critical advisory in a Jest-only path does not light up the same as one in your production bundle. For monorepos, we deduplicate across workspaces the way pnpm does, but we preserve the per-workspace path when you need to route a ticket. If you want CI gates that agree across npm, pnpm, and Yarn, run Safeguard's policy check alongside the native audit and treat the native audit as a fast smoke test.