JS Ledger

Reading a Lockfile: Supply Chain in Practice

What package-lock.json actually records, what its integrity hashes do and do not prove, and where the real install-time risk sits.

5 min read
The inside of a heavy steel lock: stacked levers and a machined bolt seated in a cast housing, with a single brass bar riding in the slot.

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 overrides in package.json to 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.

Share

Related posts

Arrow keys to move, Enter to open.