Reading `pnpm-lock.yaml`
pnpm-lock.yaml is a YAML file, but it is not always a single YAML
document. Depending on what the project uses, pnpm writes either one document
or two:
| Document | Contents |
|---|---|
| The env lockfile (first, when present) | Config dependencies and the pnpm version resolved for the project, under configDependencies and packageManagerDependencies, with the packages and snapshots entries they need |
| The project lockfile (last, always present) | The project's own dependency graph: importers with their dependencies, and the matching packages and snapshots |
Both documents declare the same lockfileVersion. That field describes the
schema of the entries inside a document, not how many documents the file
contains, so it does not change when the env document appears.
This page is for tools that read pnpm-lock.yaml — vulnerability scanners,
SBOM generators, dependency graph builders, and dependency update bots. If you
only use pnpm, you don't need to care about any of this.
A two-document lockfile looks like this:
---
lockfileVersion: '9.0'
importers:
.:
configDependencies: {}
packageManagerDependencies:
pnpm:
specifier: 12.0.0-rc.7
version: 12.0.0-rc.7
packages:
# pnpm and its platform binaries
snapshots:
# ...
---
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
importers:
.:
dependencies:
react:
specifier: ^19.0.0
version: 19.0.0
packages:
# every package in the project's dependency graph
snapshots:
# ...
Which document you need
That depends on what your tool is for:
- The project's dependency graph — dependency update bots, graph builders, anything that asks "what does this project depend on": the last document.
- Everything the lockfile installs — vulnerability scanners and SBOM
generators: every document. Config dependencies are real npm packages,
installed into
node_modules/.pnpm-config, and they appear only in the env document.
Either way, load the file with a multi-document API. A single-document one
(load() in js-yaml, yaml.safe_load in Python, yaml.Unmarshal into one
value in Go) either raises an error or silently gives you the first document —
which, in a two-document lockfile, is the env document.
The project's dependency graph
Take the last document:
import { readFile } from 'node:fs/promises'
import { loadAll } from 'js-yaml'
const lockfile = loadAll(await readFile('pnpm-lock.yaml', 'utf8')).at(-1)
This is correct for both shapes, and for every pnpm-lock.yaml pnpm has ever
written.
If you would rather detect the layout than always take the last document, a
file whose first line is --- has an env document. pnpm writes that leading
marker only when it writes two documents.
Everything the lockfile installs
Read every document and union what you find in each:
const documents = loadAll(await readFile('pnpm-lock.yaml', 'utf8'))
const installed = documents.flatMap((doc) => Object.keys(doc.packages ?? {}))
Take each document as an inventory of its own rather than merging the documents
into a single object first. Both use the . importer key, and each carries its
own packages and snapshots maps, so merging them overwrites one side's
importer and loses whichever graph it held.
Why the env document comes first
pnpm has to know which pnpm version to switch to, and which plugins to load, before it can do anything else — including before it has any reason to parse a dependency graph that may be several megabytes. Keeping that information in a small leading document means every command reads a few kilobytes rather than the whole file.
When a lockfile has two documents
The env document is written when either of these applies:
- The project has config dependencies. Their integrity checksums are project content and are always recorded.
- pnpm records the pnpm version it resolved for the project, under
packageManagerDependencies. This happens when the project declaresdevEngines.packageManager, or pins pnpm 12 or newer through the legacypackageManagerfield. SettingpmOnFailtoignoreturns it off — pnpm then doesn't enforce the pinned version, so it has nothing to record.
Two-document lockfiles have existed since config dependencies were introduced. They became common in pnpm 12, which records the resolved package manager version by default.
Since v11.23.0, a frozen install no longer rewrites this block. When the pinned
pnpm version is missing from the lockfile or no longer matches it,
pnpm install --frozen-lockfile fails with
ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE, so a manifest whose pin was
bumped without regenerating the lockfile no longer passes CI. Run
pnpm install without --frozen-lockfile and commit the result.
Scanning for vulnerabilities
A tool that reads only the first document of a two-document lockfile does not
fail loudly. The env document is a structurally valid lockfile — it just
describes an importer with no dependencies. Such a tool reports that the
project has no dependencies, and therefore no vulnerabilities, and a CI gate
built on it passes.
If you scan pnpm projects:
- Read every document, as described above. A tool that reads only the project document misses the config dependencies; a tool that reads only the env document misses everything else.
- Verify your scanner against a two-document lockfile before you trust its output. "It reported something" is not a sufficient check here: the env document does contain packages, so a broken reader emits a plausible-looking result listing pnpm's own binaries.
pnpm auditreads the lockfile correctly in both shapes and needs no configuration.