
Most developers have never read their lockfile. It is large, machine-generated, and merges badly, so it gets treated as an artifact rather than as a document.
It is worth ten minutes, because it is the only place your project states what code it actually runs — and because most advice about “supply chain security” is advice about this file, given without explaining what it contains.
Checked with npm 12.0.1 on Node 24.18.0.
What is in there
Open package-lock.json and look past the top-level metadata at the packages
object. Each entry describes one installed thing:
{
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
}
}
Four facts, and it is worth being precise about each.
version is the resolved version — what you got, not what package.json
asked for. ^1.1.0 in your manifest and 1.1.1 here is the normal state.
resolved is the URL the tarball came from. Anything that is not your expected
registry is worth a second look; this is where a misconfigured private registry
or a stale internal mirror shows up.
integrity is a Subresource Integrity string: a base64 SHA-512 of the tarball
bytes. On every subsequent install, npm hashes what it downloaded and compares.
A mismatch aborts the install.
The key itself — node_modules/picocolors — is the install path, which is how
the lockfile records nested duplicates when two dependents need incompatible
versions.
The nesting also answers “how much code is this really”. npm ls --all prints
the full tree; a mid-sized web application with twenty direct dependencies
routinely lands in four figures.
What the hash proves, and what it does not
The integrity hash proves exactly one thing: the tarball you are installing today is byte-identical to the one that was there when the lockfile was written. That is a real guarantee, and it defeats a whole class of attack — registry tampering, a mirror serving different bytes, a network-level swap.
It does not prove that the tarball corresponds to the repository it claims to come from. What gets published to npm is whatever the publisher uploaded. It can contain files the repository does not have, built from source that was never committed, and the hash will be perfectly valid. It has happened, more than once, in packages with large download counts.
Provenance is
what closes that gap. A package published from CI with npm publish --provenance carries a signed attestation, generated through Sigstore, linking
the tarball to the repository, the commit, and the workflow that built it. npmjs.com shows a provenance badge for those packages, and you
can verify locally:
npm audit signatures
That checks registry signatures and provenance attestations for your installed tree. Coverage is not universal — plenty of widely used packages still publish from a laptop — but the trend is the right one, and the command is fast enough to sit in CI.
The part that actually runs code
Here is the thing the hashes do not address at all.
preinstall, install and postinstall scripts execute during installation,
as your user, with your environment — including whatever tokens are in it. This
happens before any of your own code runs, before your tests, before any review.
A dependency six levels down the tree has the same access as one you chose.
The scripts are legitimate technology: native modules compile, and tools like
esbuild place a platform-specific binary — the same esbuild that
pre-bundles your dependencies
before the dev server serves them. But it means npm install is not a
download. It is an execution.
npm ci --ignore-scripts
That skips them all. It also breaks any package that genuinely needs one — the
usual approach is to disable scripts globally and allow-list the few that must
run. The package.json of the site you are reading does exactly that: two
entries, esbuild and fsevents, and nothing else gets to run code at install
time. pnpm expresses the same idea as onlyBuiltDependencies.
Whatever the mechanism, the useful question is: which packages in my tree run code at install time, and do I know why?
npm ci versus npm install
The distinction matters more than it looks.
npm install reconciles package.json with the lockfile and may rewrite the
lockfile — adding a package, updating a range, changing a resolution. That is
correct behavior for development.
npm ci deletes node_modules, installs exactly what the lockfile says, and
fails if the lockfile and package.json disagree. It never writes the
lockfile.
In CI, only the second is defensible. A pipeline running npm install can
silently install a version that nobody has ever tested, because a transitive
dependency published a patch release between your last commit and this build.
It belongs next to the other checks a build has to make on its own account,
type checking among them.
Two related habits:
- Commit the lockfile. Every project, including libraries. The published package ignores it; your CI does not.
- Use
overridesinpackage.jsonto pin a transitive dependency you have a reason to distrust. It is the only supported way to fix a version you do not depend on directly.
What the lockfile cannot do for you
A lockfile freezes a resolution. It does not evaluate it. If the version you locked was already compromised, the hash faithfully guarantees you the compromised bytes, forever, with a green checkmark.
The gap it leaves is time. The dangerous window for a compromised release is the first hours after publication, before anyone notices — and an automated dependency bot that upgrades within minutes runs straight into it. Some ecosystems now offer a minimum release age or a cooldown setting for exactly this reason; where the tooling does not, deliberately delaying non-security upgrades by a few days costs nothing and removes most of the exposure.
Reading the lockfile is where all of this starts, because it is the only complete list of what you are trusting. A dependency you cannot justify is a dependency you can remove, and removal is the only mitigation that works completely.