· 6 min read ·

Competing Package Managers Left Us With Competing Lockfile Formats

Source: simonwillison

Simon Willison’s recent piece on package managers frames the problem as a velocity issue: too many tools shipping too fast, with too little regard for developers downstream. That framing is right, but the most concrete cost of that velocity is one that gets less attention than install-time security or ecosystem fragmentation. It is the lockfile.

The lockfile is the artifact that makes reproducible builds possible. It is also, increasingly, a proprietary format that ties you to a specific tool, breaks when you upgrade that tool, and cannot be read by the audit tooling built for a competing implementation. Every time the package manager ecosystem fragments, the lockfile fragments with it.

What a Lockfile Is Supposed to Do

The core promise is simple: given the same lockfile and the same registry state, you get the same dependency tree. It turns the inherently non-deterministic act of resolving ^4.0.0 into a specific commit: version 4.2.1, from this registry URL, with this integrity hash.

That integrity hash is the security-critical part. npm packages record a sha512 hash in the lockfile:

{
  "node_modules/some-package": {
    "version": "1.2.3",
    "resolved": "https://registry.npmjs.org/some-package/-/some-package-1.2.3.tgz",
    "integrity": "sha512-abc123..."
  }
}

When npm install runs with a lockfile present, it checks the downloaded tarball against the recorded hash. If the registry serves a different tarball for the same version, the install fails. This is the primary defense against dependency confusion attacks, where an attacker publishes a malicious package to the public registry with the same name as an internal package. The lockfile works when you commit it to version control, review changes alongside code changes, and regenerate it deliberately rather than accidentally.

The Format Zoo

The problem is that no standard lockfile format exists. Every major package manager ships its own:

  • npm: package-lock.json, currently at lockfile version 3 (introduced in npm 7, structurally incompatible with the v1 format used by npm 5 and 6)
  • Yarn Classic: yarn.lock, a custom key-value format specific to Yarn
  • Yarn Berry: also named yarn.lock, but with a different internal structure not backward-compatible with Yarn Classic
  • pnpm: pnpm-lock.yaml, a YAML file with a pnpm-specific schema that encodes its content-addressable store layout
  • Bun: originally bun.lockb (a binary format), switched to a text-based bun.lock in Bun 1.2 in January 2025

Python runs a parallel fragmentation: pip has no built-in lockfile, pip-tools generates requirements.txt-style lockfiles, Poetry uses poetry.lock in TOML, PDM uses pdm.lock in TOML with a different schema, and uv uses uv.lock, also TOML but optimized for its own resolution model. Each format is internally consistent and each represents real design work. None of them interoperate.

The Migration Penalty

Switching from one package manager to another means discarding your existing lockfile and regenerating it from scratch. This sounds minor until you consider what gets discarded.

A mature project’s lockfile encodes months or years of dependency decisions. Some packages are pinned below their latest version because a higher version introduced a breaking change the team discovered in production. Some transitive dependencies sit at specific versions because a security advisory required an explicit override. Upgrade hooks in CI may depend on the lockfile format to detect when dependencies changed.

When you regenerate the lockfile with a new tool, all of those decisions get re-evaluated. The new lockfile may resolve to different versions. The differences may not surface until something breaks. The practical consequence is that switching package managers is not a refactoring task; it is a re-integration test. Teams that understand this tend to stay on whatever tool they started with, regardless of the improvements a newer tool would offer.

This is part of what Willison means by cooling down. The benchmarks that justify switching from npm to pnpm, or from pip to uv, typically measure cold-cache resolution on a clean install. They do not measure the cost of auditing the lockfile diff, updating CI configuration, and verifying that production behavior is unchanged across the migration.

Bun’s Binary Lockfile and What It Revealed

The original bun.lockb format made the trade-offs unusually visible. A binary lockfile cannot be reviewed in a pull request. A diff like:

Binary files a/bun.lockb and b/bun.lockb differ

tells you nothing about which dependency changed, why, or whether the change is expected. All the security benefits of committing a lockfile, which rest on humans and automated tools being able to read it, disappeared.

Bun moved to the text-based bun.lock in version 1.2, which is the correct decision. The interesting thing is that the original design choice existed at all. Binary formats are faster to parse, which serves Bun’s core performance value proposition. Readable formats are safer to audit. The first version of the format prioritized speed. The community pushed back, and readability won the second round. That pushback took months and required a migration path for projects already using bun.lockb.

The Audit Tooling Problem

Security audit tools are lockfile-format-specific by necessity. npm audit reads package-lock.json. yarn audit reads yarn.lock. The OSV Scanner from Google supports multiple formats, but it maintains separate parsers for each one, each of which needs updating when the underlying format evolves. Dependabot supports multiple lockfile formats through an explicit list of integrations, each maintained independently.

When npm released the v3 lockfile format in npm 7, tooling that parsed the v1 and v2 formats needed updates. Some tools had incorrect behavior for the new format for months before catching up. In a space where the lockfile is the primary artifact for verifying what is actually installed, format instability is not just an inconvenience. It creates windows where the tooling you depend on for security review is operating on stale assumptions about the file it is parsing.

Cargo.lock as a Reference Point

Cargo handles this with one format, maintained carefully, with backward compatibility treated as a commitment rather than a goal. Cargo.lock is TOML, human-readable, and its schema has evolved with explicit versioning:

[[package]]
name = "some-crate"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abc123..."
dependencies = [
 "other-crate 0.4.1",
]

Every field has a clear meaning. A Cargo.lock file from several years ago is still parseable by the current Cargo version. Security tooling like cargo-audit and cargo-deny builds on a stable format and does not need to track a fragmented set of competing schema versions.

The Rust ecosystem has one package manager and one lockfile format. This is not a coincidence, and it is not an accident. It is the outcome of shipping a single integrated toolchain and maintaining it as a platform rather than a competitive market.

What Stability Would Look Like

For the JavaScript ecosystem, treating the lockfile format as a stability commitment would mean a few concrete things. Format versions should be additive and backward-compatible, not require wholesale regeneration on tool upgrade. Binary formats are inappropriate for a security-critical artifact that belongs in version control. Changes that invalidate existing lockfiles should require major version signals with clear deprecation windows.

For Python, the proliferation of *.lock formats reflects the genuinely decentralized nature of the ecosystem, which is harder to address. But the tooling layer could do more to build on the standardization that PEP 517, PEP 518, and PEP 621 established for build metadata. A lockfile interchange format that any compliant tool could read and write would reduce the migration cost without requiring the ecosystem to converge on a single tool.

The lockfile is a small artifact with a large surface area. It sits at the intersection of reproducibility, security, and developer workflow. The package management ecosystem has treated it as a private implementation detail of each tool. What Willison is describing when he says to cool down is, at least in part, an argument for treating shared artifacts as shared infrastructure, with the stability guarantees that implies.

That is a reasonable bar. The current state of the ecosystem falls well short of it.

Was this interesting?