Package format specification

Pin a dependency's subgraph ahead of time.

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.

specifier grammar
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

Maintainers still want to pin what they ship.

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.

An input, not a mandate

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.

Integrity isn't bypassed

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.

Scoped, so dedupe still works

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.

Degrades to the bare version

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.

Portable, not a sidecar

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.

Canonical and auditable

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.

This is a distributable resolution seed.

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.

The graph is the payload.

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.

01

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.

02

Same graph, same bytes

An identical subgraph always encodes to the same bytes and the same SHA-256, so two locks are equal by comparing one hash.

03

Scoped to one dependency

It pins a single dependency's transitive closure rather than the whole project, so each dependency can carry its own pinned subgraph.

04

Works with today's tools

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.

05

The tool stays in charge

A consumer can honor, ignore, override, remap sources, or reject the lock under its own configuration; it is an input, not a mandate.

06

Integrity rides along

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

A lock pins a dependency's subgraph before lockfile generation.

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.

  1. 1
    Maintainer's lock “Append this string; we've tested this subgraph.” A distributed, pinned resolution.
  2. 2
    Package manager vlt honors it by default; a lock-unaware tool ignores it and installs the bare version.
  3. 3
    User configuration Overrides, source remaps, security policy, and install mode have the final say, and can reject it outright.
Scope

Applies to the transitive/subgraph rooted at a specific dependency, not to unrelated siblings in the project graph.

Override point

When honored, supplies resolved dependency targets before user-level configuration is applied.

User policy

Package-manager config remains authoritative: overrides, source remaps, security policy, and install modes can still change or reject the result.

Integrity

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

A small nodes-and-edges graph, scoped to one subtree.

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.

1

Capture the subgraph

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.

2

Encode canonically

Serialize that graph to a deterministic binary blob: a columnar layout with structural versions, varints, and delta-encoded edges. Not JSON.

3

Compress & frame

Brotli when it pays off (tiny graphs ship uncompressed), then prepend a one-byte frame that identifies the format and codec.

4

Armor as Base62

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 graphBrotliframe byteBase62npm:name@version+lock.<payload>

Decoded payload

Decoded, it's a small nodes-and-edges graph.

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.

decoded · npm:lightningcss@1.32.0 subtree
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

Generate a lock in your browser.

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

A dependency value can carry the locked subgraph.

package.json
{
  "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

Implemented by vlt first. Portable by specification.

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.

vlt

Target implementation. Can choose to decode, validate, and use the locked subgraph during resolution.

Other package managers

May adopt the format. Until they do, they should be described as lock-unaware, not lock-compatible.

Existing SemVer parsers

Mostly relevant to transport. Accepting +lock.<payload> is not the same as honoring Dependency Locks.

Sidecar path

Package managers that reject source-qualified build metadata can still carry the same graph outside the version string.

Security model

Fail closed at the artifact boundary.

Tamper-evident

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.

Bounded decoding

Canonical parsing plus size, node, edge, ratio, and fetch fan-out caps defend against malicious payloads and amplification.

The right boundary

Stale, malformed, or future-format locks degrade to the still-valid bare version. Artifact substitution still fails the normal integrity boundary.

The specification

Read the spec, then argue with the details.

The full specification covers source-qualified identifiers, ABNF grammar, binary layout, resolution semantics, consumer policy, publishing behavior, security analysis, and transport compatibility tests.