pnpm 12 Quietly Blinds Your Dependency Scanner (and the Fix Has a Missing Step)

Your lockfile still pins the vulnerable versions, and your scanner still says "No issues found"

On 2026-10-04 I was moving a repository I maintain from Yarn Classic to pnpm 12.9.1. Boring work: delete three yarn.lock files, pin "packageManager": "pnpm@12.9.1" in each package.json, regenerate, update the setup docs. By my own note from that day, the repository had 160 open Dependabot alerts on main.

If that migration had merged as-is, those 160 alerts would have closed themselves as "fixed." No dependency would have changed. This is the story of why, and of the one step the official workaround doesn't do for you.

The News: pnpm 12 Writes Two Documents Into Your Lockfile

This was reported to pnpm as pnpm/pnpm#13805 on 2026-08-11, against pnpm 12.0.0-rc.3.

The short version: when package.json sets packageManager, pnpm 12 records its own resolved version in an "env document" and writes it first in pnpm-lock.yaml, ahead of your real dependency graph. The env document itself isn't new — the issue points out pnpm already wrote one for config dependencies. What changed in 12 is the trigger: now any project that sets packageManager gets one.

Here is the top of the lockfile from my migration branch, before I fixed it:

---
lockfileVersion: "9.0"

importers:
  .:
    configDependencies: {}
    packageManagerDependencies:
      pnpm:
        specifier: 12.9.1
        version: 12.9.1

packages:
  "@pnpm/exe.android-arm64@12.9.1":

The whole env document is 158 lines, nearly all of it pnpm's own platform binaries. Then a second ---, then a second lockfileVersion: '9.0', and only then the dependencies I actually ship.

Both documents declare the same lockfile version, which is the cruel part. A tool that implements lockfile v9 gets no signal that the file's shape changed. Read the first document and you'll conclude the project depends on pnpm and nothing else.

Who Goes Blind, Tool by Tool

I'm keeping each claim tied to whoever actually measured that tool, because "scanners break" is too vague to act on.

On GitHub's side, the issue's reporter merged their pnpm 12 bump at 01:55:21Z on 2026-08-11. The dependency graph reported dependenciesCount=0 for pnpm-lock.yaml, and 27 open Dependabot alerts closed as "fixed" at 01:55:27–28Z. Six seconds. (Their follow-up comment has a nasty detail: once the alerts reopened and later closed for real, the API kept only the final fixed_at. The blind window leaves no trace afterwards unless you captured the alert list during it.)

For local scanners, a comment on the same issue on 2026-08-11 — posted from the account of zkochan, who answers for pnpm in that thread, and marked as written by Claude Code — tested fixtures made by pnpm 12.0.0-rc.3 with three known-vulnerable packages:

  • pnpm audit (12.0.0-rc.3) reported 33 advisories with either lockfile shape.
  • Trivy 0.73.0 reported 34 vulnerabilities with either shape.
  • osv-scanner 2.5.0 found 3 vulnerable packages on the single-document file and exited 1. On the two-document file it reported "No issues found" and exited 0.
  • Syft 1.51.0 produced an SBOM for the two-document project listing 9 components, all of them pnpm's own binaries.

That osv-scanner line is the one that should worry you. It doesn't crash. It doesn't warn. It returns a populated, plausible result, and a CI gate built on it goes green.

Some of this has moved since. The osv-scalibr extractor issue for this (google/osv-scalibr#2351) was closed on 2026-09-15 as already fixed by commit 87a1418f (committed 2026-08-28), which taught the extractor to read the full YAML stream. osv-scanner v2.6.0, released 2026-09-14, pins an osv-scalibr commit that contains that change — I have not re-run the fixtures against it. I have not checked whether Syft or GitHub's dependency graph reads the second document today, so I would not assume either one does.

pnpm's Answer: The Layout Stays

The issue proposed putting the main document first. pnpm declined, and the reasoning is worth reading rather than paraphrasing badly. Swapping the order would break the tools that had already added support and read document 2. The small leading document lets pnpm learn which pnpm version and plugins to load without parsing a multi-megabyte graph.

The advice to tool authors is: take the last document — yaml.loadAll(content).at(-1) — or better, read both and union them, because config dependencies live in the env document too and are real packages.

The issue was closed on 2026-08-23 with this:

There is nothing we can do at this point. The change has already shipped and it has shipped several months ago. It is now the responsibility of those tools to make the change.

So if your scanner hasn't caught up, the fix is on your side of the fence.

The Workaround, and the Step It Skips

The supported way to get a single-document lockfile back is one line in pnpm-workspace.yaml:

pmOnFail: ignore

With that, pnpm stops enforcing the package-manager version, so it has nothing to record. The reporter also verified that pmOnFail: error does not work — pnpm still resolves the version to compare it, so it still writes the env document. And per pnpm's reply, projects that use config dependencies will get two documents either way.

So I added pmOnFail: ignore to the root and both functions-package pnpm-workspace.yaml files, ran pnpm install --lockfile-only, and checked. Two documents.

Then I ran it again with --pm-on-fail=ignore on the command line, while pnpm config get pmOnFail already printed ignore. Still two documents. (At this point I was arguing with a YAML file.)

Here's the missing step, measured on pnpm 12.9.1 on 2026-10-04: the setting stops pnpm from writing a new env document, but it does not remove one that is already there. What worked was deleting everything up to the second --- by hand and then running pnpm install --lockfile-only once more. pnpm kept the file as a single document, pnpm install --frozen-lockfile still passed, and the diff was exactly the 158-line env prefix. The fix went into the same PR as the migration, so the two-document lockfile never reached main.

The check I'd leave in your notes, or your CI:

grep -c '^---$' pnpm-lock.yaml

It should print 0. On that repository's main today, it does.

What I'm Taking Away

A green scanner is evidence only about what the scanner read. "It reported something" isn't a passing check either: the broken tools here didn't return empty output, they returned pnpm's own packages. If you upgraded to pnpm 12 and set packageManager, run that grep today, then look at the dependency count GitHub shows for your lockfile. If it's suspiciously small, your alerts may have been quietly closing on you.

Did your scanner catch this, or did it say "No issues found"? I'd like to hear which tool and which version — that's exactly the data this mess is short on.