
This is the third installment of what is apparently becoming a series about CVE hygiene. First, I wrote about properly and safely resolving CVEs in your dependencies: update the vulnerable package within its existing range, then update the parent that pulls it in, and only reach for resolutions/overrides as a last resort. Then I wrote about the Bun CVE gap, where I discovered that Bun doesn't give you any of those options, and that pnpm and npm both handled my test scenario correctly.
Today, pnpm gets its own post: it shipped a proper pnpm audit --fix, with a mode that does exactly the surgical update I've been advocating for, plus a really thoughtful interaction with minimumReleaseAge that I haven't seen anywhere else.
Let's recap the baseline. A CVE lands on a transitive dependency, the parent accepts a wide range, and a patched version exists inside that range. As demonstrated in the Bun post, pnpm handles this the way you'd expect:
pnpm update qs
The transitive gets re-resolved in place, the parent stays put, package.json is untouched, and the diff is contained to the lockfile. No accidentally promoted direct dependency, no regenerated tree. This is the primitive that Bun doesn't have, and it's the foundation everything else builds on.
pnpm audit --fixManually tracing every advisory through your tree gets old fast, so let's talk about pnpm audit --fix.
The command makes you pick a method, and the one the docs lead with is --fix=override, which adds overrides to pnpm-workspace.yaml to force non-vulnerable versions.
I know, I know. Overrides? That sounds exactly like the last-resort tool I've been warning about since the original post. But there are two nuances worth looking at.
pnpm's overrides support range selectors, so instead of the blunt "lodash": "^4.17.21" that redirects every consumer in your tree, you get entries like:
overrides:
"lodash@<4.17.21": ">=4.17.21"
That's the version-keyed resolutions pattern from my original post, expressed the pnpm way. It's still an override, it's still sticky, and you should still plan to remove it once your tree naturally resolves past the vulnerable range. But it's scoped to the vulnerable versions, which beats the "every copy of this name becomes this version" behavior I complained about in the Bun post.
Since pnpm 11, overrides live in pnpm-workspace.yaml rather than package.json, which also means they're centralized for the whole workspace instead of scattered across manifests.
--fix=updateThe other method, added in v11.0.0:
pnpm audit --fix=update
--fix=update is the surgical workflow, automated: re-resolve the vulnerable packages to patched versions, no overrides involved. For transitives, the diff stays in the lockfile. When a direct dependency is itself vulnerable, pnpm bumps its range in package.json, same as a regular pnpm update. Use it by default, and reserve --fix=override for the cases where updates within range genuinely can't fix it.
Add --interactive (or -i, also since v11.0.0) to review the advisories one by one and pick which fixes to apply. Useful when you want to be deliberate about each change rather than applying everything at once:
pnpm audit --fix=update --interactiveminimumReleaseAge interactionNow for the detail that made me want to write this post in the first place.
If you've set up minimumReleaseAge (and you should, it's on by default since pnpm 11), you've told pnpm to refuse installing any version published more recently than your configured window. Great for dodging compromised releases. But there's an obvious tension: what happens when a security fix was published within that window? Do you wait one to three days while a known CVE sits in your tree?
pnpm's answer: when minimumReleaseAge is set, pnpm audit --fix also adds the minimum patched version of each advisory to minimumReleaseAgeExclude in pnpm-workspace.yaml:
minimumReleaseAge: 1440
minimumReleaseAgeExclude:
- qs@6.15.2
The security fix installs immediately, while everything else still goes through the normal aging process.
The design gets two things right:
Your default posture stays conservative, and the escape hatch is applied narrowly, per-advisory, per-version. That resolves the "update fast vs. update safely" tension I complained about in the minimum release age post.
If you're on pnpm and a CVE lands in your tree, the workflow I'd reach for today looks like this:
pnpm audit --fix=update --interactive to re-resolve vulnerable packages in the lockfile, reviewing each fix as you go.minimumReleaseAgeExclude when the fix is too fresh for your release age window.--fix=override for advisories that can't be resolved by an update within range, and scope them to the vulnerable versions.pnpm audit to confirm, and test your project before calling it done.After spending a whole post complaining about a package manager that can't do surgical updates, it's only fair to spend one on the package manager that's quietly becoming the best at it. Between in-place transitive updates, --fix=update, range-scoped overrides, and the minimumReleaseAgeExclude interplay, pnpm is essentially encoding the entire workflow from my original post into its tooling.