# 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.

- Published: 2026-08-26
- Tags: nodejs, tooling
- Source: https://jsledger.com/blog/reading-a-lockfile/
- Language: en-US
- Author: Jonah Vail

---
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:

```json
{
	"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](https://docs.npmjs.com/generating-provenance-statements)** 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:

```bash
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.

:::note
`npm audit` and `npm audit signatures` answer different questions. The first
compares your versions against a database of known vulnerabilities. The second
asks who published these bytes and from where. A package can be clean by one
measure and unsigned by the other.
:::

## 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](/blog/what-happens-when-vite-serves-a-module/)
before the dev server serves them. But it means `npm install` is not a
download. It is an execution.

```bash
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](/blog/type-level-narrowing-in-typescript/).

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.
