Lockfiles
Date: 2026-08-17
A record of exactly which versions were resolved, so every install produces the same tree. Committing it is not a preference — without one, two people running the same command on the same day can get different code.
A lockfile records the fully-resolved dependency tree: every package, its exact version, its resolved URL, and an integrity hash.
package.json what you WANT ^4.17.0
lockfile what you GOT 4.21.4
+ every
transitive
dependency
Why it’s not optional
Version ranges mean the resolved tree depends on when you install.
Monday you install → lodash 4.21.4
Tuesday 4.21.5 is published
Tuesday colleague installs → lodash 4.21.5
same package.json, different code
Without a lockfile, “works on my machine” is structurally guaranteed — and it applies to every transitive dependency, which is where most of the tree lives and where you have no version control at all.
With a lockfile: the tree is fixed until someone deliberately changes it, and that change appears in a diff someone can review.
What’s inside
"node_modules/lodash": {
"version": "4.21.4",
"resolved": "https://registry.npmjs.org/...",
"integrity": "sha512-v2kDEe57..."
}The integrity hash is the security-relevant part. It’s a hash of the package contents, verified on install — so a package silently altered at the registry fails rather than installing — Hashing, Supply Chain Risk.
Install strictly in CI
npm ci # not: npm install
pnpm install --frozen-lockfile
yarn install --immutablenpm install | npm ci | |
|---|---|---|
| Can update the lockfile | Yes | No |
Fails if it disagrees with package.json | No | Yes |
Deletes node_modules first | No | Yes |
| Speed | Slower | Faster |
npm install in CI defeats the lockfile, because it will happily resolve something new and carry on. npm ci is the correct command in every automated context.
Reviewing lockfile changes
The diff is enormous and mostly unreadable, which is why it gets rubber-stamped. Two things are worth checking anyway:
- Did a package appear that nobody added? A new transitive dependency arriving with a minor bump is normal and worth noticing
- Does the change match the intent? A one-line
package.jsonbump producing 400 lockfile changes means a transitive tree moved substantially
npm ls --all | less # the resolved tree
npm explain lodash # WHY is this here?npm explain is the underused one — it answers “which of my dependencies pulled this in”, which is the question every unexpected package raises.
Common problems
- Merge conflicts. Two branches adding dependencies conflict in the lockfile. Never hand-edit it — regenerate:
git checkout --theirs package-lock.json
npm install # re-resolve from package.json- Committing
package.jsonwithout the lockfile. The declared range changes, the resolution doesn’t, and the two disagree until someone runs a plain install - Mixed managers. Both
package-lock.jsonandpnpm-lock.yamlin one repo means two different trees depending on who installs — Package Managers .gitignore-ing it. Occasionally done deliberately for libraries, and wrong for applications. See below
Applications versus libraries
The one legitimate nuance:
- Applications commit the lockfile. You control the deployment; reproducibility is the whole point
- Libraries commit it too — for their own CI — but consumers ignore it. A library’s lockfile doesn’t constrain the projects that depend on it, which is why libraries should use ranges in
package.jsonrather than exact pins — Versioning
The old advice that libraries shouldn’t commit lockfiles predates the security argument. Committing it gives the library’s own CI reproducibility and integrity checking, and costs consumers nothing.
The one-line summary
Commit it, install from it in CI, and treat changes to it as reviewable. Those three habits eliminate an entire class of environment bug and most of the passive supply-chain exposure.