Nothing extra to commit
The resolved subgraph travels inside the dependency value itself, so there is no separate lockfile to generate, share, or keep in sync.
Package format specification
A Dependency Lock captures one dependency's resolved subgraph as a portable, shareable value. Resolve it once; any aware tool pins that slice of the graph to it.
npm:express@5.2.1+lock.163pGWiielRlPY5wPsWb7DtsYkTwvpFTO8OPltrZZ7s6pBrPgoOZ…
npm:express@5.2.1 is the package you already install.
+lock.… is the only new part:
a pinned, shareable copy of the resolved subgraph, computed ahead of time.
A specification from vlt, shipping with the vlt client. Not a claim of ecosystem-wide package-manager support.
Why this exists
npm-shrinkwrap.json was the one widely-deployed way to
publish a locked dependency tree with a package, and npm is removing it in
its next major (v12). The demand never went away. When a tool like the
Pi coding agent ships as a global npm install, its authors want to
guarantee the exact transitive graph they tested, and shrinkwrap was how they did it.
People are now asking for a --locked-style install, declarative builds
like Nix leaned on shrinkwrap, and CLI authors still ship generated shrinkwraps to
harden their supply chain.
But shrinkwrap earned its bad reputation: in a published package it is hostile to consumers: it freezes the whole tree, blocks deduping, and stops people from patching through security fixes, while quietly bypassing normal install rules. A maintainer wants to say “append this string; we've tested this subgraph” and have it respected, without taking the final say away from the person installing. A Dependency Lock is that: a tested resolution the maintainer distributes, applied as one layer the consumer still overrides.
Shrinkwrap froze the consumer's tree and blocked security updates. A lock is a seed: the consumer's tool can honor, override, re-resolve, or reject it, and package-manager config stays authoritative.
Shrinkwrap could pull unsigned code from any host and slip past SCA tools. A lock can pin each artifact's integrity as tamper-evidence, and your package manager still independently verifies every artifact through its normal metadata and fetch checks.
Shrinkwrap pinned the entire project and prevented deduping. A lock pins one dependency's subgraph, so the installer can still flatten and dedupe across the rest of the tree.
A stale, hand-maintained shrinkwrap quietly trapped you on old, vulnerable versions. A stale, malformed, or rejected lock falls back to the plain version and re-resolves.
Shrinkwrap was an npm-specific file dropped next to the package. A lock rides inside the dependency value across tools and registries, and node identity carries its source so the same name@version from different registries stays distinct.
A large JSON tree is hard to review and diff. A lock is a deterministic binary graph with a single digest for equality, caching, and tamper-evidence.
Dependency Locks define how maintainers can publish not just a package version, but the resolved dependency subgraph rooted at that version. Aware tools can use it before generating their native lockfile.
That subgraph is just a plain graph (resolved package instances and typed edges) narrowed to one rooted dependency. It fixes that dependency's transitive choices, not the whole project graph, install layout, or config.
The resolved subgraph travels inside the dependency value itself, so there is no separate lockfile to generate, share, or keep in sync.
An identical subgraph always encodes to the same bytes and the same SHA-256, so two locks are equal by comparing one hash.
It pins a single dependency's transitive closure rather than the whole project, so each dependency can carry its own pinned subgraph.
The marker rides in standard SemVer build metadata, so a tool that doesn't understand it still installs the plain npm:express@5.2.1.
A consumer can honor, ignore, override, remap sources, or reject the lock under its own configuration; it is an input, not a mandate.
A lock can carry each artifact's integrity hash as advisory tamper-evidence; it never replaces verification. Your package manager still independently checks every artifact it fetches.
Resolution semantics
If a package manager supports Dependency Locks and chooses to honor one, the lock supplies the resolved dependency definitions for that rooted subtree before the consumer's native lockfile is generated. It is then still subject to user-level configuration, policy, registries, overrides, and the package manager's own installation model.
Resolution cascades, like CSS: the layer closest to the consumer wins.
Applies to the transitive/subgraph rooted at a specific dependency, not to unrelated siblings in the project graph.
When honored, supplies resolved dependency targets before user-level configuration is applied.
Package-manager config remains authoritative: overrides, source remaps, security policy, and install modes can still change or reject the result.
A lock may carry artifact integrity as advisory tamper-evidence; it doesn't replace verification. The package manager still independently enforces integrity through its normal lockfile and fetch pipeline.
The format
The payload is its own compact graph format: just resolved
nodes and typed edges for one rooted subtree, no
config. It's modeled on
vlt's lockfile
graph and maps cleanly to and from it, but it's deliberately generic. Each node
is one origin-qualified id like npm:react@1.2.3, with no
tool-specific encoding, so any package manager can implement it. The version
stays ordinary SemVer; npm: just supplies the origin.
Resolve one rooted subtree into a plain graph of nodes and typed edges, plus optional per-node integrity and platform conditions, with no config. It maps cleanly onto vlt's graph.
Serialize that graph to a deterministic binary blob: a columnar layout with structural versions, varints, and delta-encoded edges. Not JSON.
Brotli when it pays off (tiny graphs ship uncompressed), then prepend a one-byte frame that identifies the format and codec.
Base62 keeps the version legal SemVer. That helps with transport, but does not mean the package manager supports the lock semantics.
npm:name@version+canonical graph→Brotli→frame byte→Base62→npm:name@version+lock.<payload>
Decoded payload
The +lock.<payload> blob is compact binary built for transport.
Decode it and you get a plain graph: each node is an
origin-qualified id like npm:lightningcss@1.32.0, optionally
carrying its artifact integrity (#sha512-…) and platform conditions
([os=… cpu=…]); each edge is just
from -> to = type (prod, optional,
peer). Nothing tool-specific; it maps one-to-one onto vlt's lockfile
graph, but anyone can read or emit it.
format = 2
root = 0
[nodes]
0 = npm:lightningcss@1.32.0#sha512-NXYBzinN… [engines=>=12]
1 = npm:detect-libc@2.1.2#sha512-Btj2BOOO… [engines=>=8]
2 = npm:lightningcss-darwin-arm64@1.32.0#sha512-RzeG9Ju5… [os=darwin cpu=arm64]
3 = npm:lightningcss-linux-x64-gnu@1.32.0#sha512-V7Qr52Ih… [os=linux cpu=x64 libc=glibc]
[edges]
0 -> 1 = prod
0 -> 2 = optional
0 -> 3 = optional
A deliberately small, generic shape: origin-qualified node ids, optional
#integrity and [platform] annotations, and
from -> to = type edges, nothing tool-specific. Integrity is
advisory tamper-evidence; your package manager still verifies every artifact. It
translates one-to-one to vlt's lockfile graph (and back), but is simple enough for
any package manager to implement. The lock digest is taken over the canonical
encoding of exactly this graph, so the same subtree always yields the same lock.
Try it
Type any public npm package. This resolves its registry subgraph live against
registry.npmjs.org, then runs the real pipeline (canonical binary
→ SHA-256 digest → Brotli (or stored) → frame byte → Base62) and hands back a
copy-pasteable +lock.<payload>. No server: it all runs here.
Non-registry dependencies (git, file:, workspace:) are
skipped and listed below.
Manifest example
{
"name": "my-app",
"dependencies": {
"express": "npm:express@5.2.1+lock.<payload>",
"lodash": "npm:lodash@4.18.1+lock.<payload>"
}
}
An aware package manager running prefer can decode, validate, and
absorb each closure into its native lockfile. A lock-unaware tool simply installs
npm:express@5.2.1 / npm:lodash@4.18.1.
Support status
Dependency Locks are a new concept. The site should not imply npm, pnpm, yarn, or bun understand or honor the lock. The measured compatibility claim is only about transport: exact SemVer versions can carry build metadata without necessarily breaking existing tools.
Target implementation. Can choose to decode, validate, and use the locked subgraph during resolution.
May adopt the format. Until they do, they should be described as lock-unaware, not lock-compatible.
Mostly relevant to transport. Accepting +lock.<payload> is not the same as honoring Dependency Locks.
Package managers that reject source-qualified build metadata can still carry the same graph outside the version string.
Security model
Each node can pin its artifact integrity, and root-match blocks pasting a foreign closure under another name. The host package manager's integrity pipeline still independently catches substituted artifacts.
Canonical parsing plus size, node, edge, ratio, and fetch fan-out caps defend against malicious payloads and amplification.
Stale, malformed, or future-format locks degrade to the still-valid bare version. Artifact substitution still fails the normal integrity boundary.
The specification
The full specification covers source-qualified identifiers, ABNF grammar, binary layout, resolution semantics, consumer policy, publishing behavior, security analysis, and transport compatibility tests.