# Lockfile (/docs/packages/lockfile)



LPM CLI's lockfile starts with &#x2A;*`lpm.lock`*&#x2A;: the TOML, git-diffable, authoritative graph. When the graph fits the current binary subset, LPM CLI also writes &#x2A;*`lpm.lockb`**, a generated binary companion. Commit `lpm.lock` always; commit `lpm.lockb` when LPM CLI writes it.

This page is the conceptual overview — when each file gets read, what the dual format buys you, and how the warm-install fast paths work. For the on-disk schema, see [`lpm.lock` format](/docs/reference/lockfile-format) and [`lpm.lockb` format](/docs/reference/lockb-format).

## Why two files [#why-two-files]

| File        | Optimized for                                           | When it's read                                                                         |
| ----------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `lpm.lock`  | Humans, git diffs, authoritative install input          | Read by install, audit, graph, and review workflows                                    |
| `lpm.lockb` | Generated binary companion for the representable subset | Used for binary validation/writeback and cheap generated-cache inspection when present |

The TOML lockfile is the canonical source — sorted, deterministic, git-friendly. The binary companion is intentionally smaller: its v3 wire format represents fixed-width package records plus sparse verified-provenance records. If the TOML lockfile contains importer snapshots, patch records, aliases, peer pinning, dependency engine constraints, platform metadata, optional-reachability state, catalogs, or registry signatures, LPM CLI skips `lpm.lockb` and removes any stale binary file.

You don't choose the mode manually. Edit `lpm.lock` by hand if you ever need to (humans do that sometimes); the next install either regenerates a compatible `lpm.lockb` or leaves the project TOML-only.

## What's pinned [#whats-pinned]

Each entry in the lockfile records:

* **Exact resolved version** — never a range
* **Source registry** (e.g. `registry+https://registry.npmjs.org`)
* **SRI integrity hash** (`sha512-…`) — verified on every install
* **Platform metadata** (`os`, `cpu`, `libc`) and optional reachability
* **Registry signature evidence** (`registry-signatures`, `registry-published-at`) when npm metadata provides it
* **Verified provenance evidence** — publisher/workflow identity, exact npm package URL, tarball SHA-512 binding, transparency-log coordinates, certificate digest, and original bundle digest
* **Patch records** (`path`, patch-file `sha256`, `original-integrity`) when `lpm.patchedDependencies` changes installed bytes
* **Direct dep names + versions** — for transitive walking
* **Tarball URL hint** — cached resolved URL that lets warm installs skip the per-package metadata round-trip
* **Importer snapshot** — the package.json dependency sections and resolver-affecting settings that frozen installs compare before replaying the lockfile
* **(if any)** npm-alias edges — `local_name → target_canonical_name` pairs

After `lpm install`, two further files exist:

* `lpm.lock` — commit it
* `lpm.lockb` — commit it when present (mark it as binary in `.gitattributes` — `lpm init` does this automatically)

## Install fast paths [#install-fast-paths]

The install pipeline has a tiered fast-path hierarchy. Each tier skips more work than the next.

### Tier 1: Up-to-date install (\~14 ms) [#tier-1-up-to-date-install-14-ms]

If the install-hash file (`.lpm/install-hash`) matches the current state of `package.json`, the authoritative lockfile, and the resolved linker mode, install exits immediately. Nothing to do.

This is the "I just ran `lpm install` and now I'm running it again" case. Hits in CI between consecutive `lpm install` calls in a script. The hash check is sync — file stat + timestamp comparison — so the entire `lpm install` invocation finishes in a few milliseconds on small projects and around 14 ms on the VitePress docs benchmark fixture.

### Tier 2: Warm install (\~387 ms) [#tier-2-warm-install-387-ms]

If `lpm.lock` matches `package.json` (no drift), but `node_modules/` is missing or stale, the install rebuilds `node_modules/` directly from the lockfile + the [global content-addressable store](/docs/packages/content-addressable-store).

No metadata fetches. No tarball downloads. Project `node_modules/<pkg>` is created as a symlink into the matching link entry under `~/.lpm/store/v2/links/<graph-key>/`. The link entries live in the **global** store, not in the project, so `rm -rf node_modules` between iterations only loses the cheap project-side symlinks — the canonical extracted bytes and the per-graph wrappers stay put. Warm install is a symlink rebuild over already-materialized link entries.

[`lpm fetch`](/docs/packages/fetch) is the lockfile-only way to create that warm store state ahead of time. It reads `lpm.lock`, downloads compatible remote tarballs into the store, and leaves `package.json`, `node_modules`, and the lockfile untouched.

### Tier 3: Lockfile-only install (offline-able) [#tier-3-lockfile-only-install-offline-able]

`lpm install --offline` forces this path. The resolver is skipped entirely; every dep is resolved from `lpm.lock`'s pinned versions, fetched from the global store (or errored if missing). No network. No re-resolution.

This is the CI-recommended path:

```bash
lpm install --offline --strict-integrity
```

`--strict-integrity` tightens the contract further: tarball-URL deps must declare their SRI inline. Trust-on-first-use is disabled.

### Tier 4: Resolve-and-install (cold) [#tier-4-resolve-and-install-cold]

If the lockfile drifts from `package.json` (a new dep was added, a range was bumped), the resolver runs. The streaming dispatcher fetches metadata, picks versions, downloads tarballs, populates the store, links into `node_modules/`, writes a fresh lockfile.

This is the slow path, but "slow" is relative — cold install on the 535-package VitePress docs fixture is **2,945 ms** in our benchmarks (vs npm 17,354 ms / pnpm 6,125 ms).

## Lockfile-version [#lockfile-version]

The TOML lockfile is **schema-versioned** (current: `7`), not tool-versioned. A version bump only happens when the on-disk shape changes. Older clients reading a lockfile with a higher version refuse to install rather than silently misinterpreting fields — fail-fast over silent breakage. Version `5` added importer snapshots and patch checksum records, version `6` added per-package `engines.node` constraints, and version `7` added artifact-bound Sigstore provenance evidence.

The binary lockfile has its own wire-format version (current: `3`). Version `2` added the per-entry `tarball_off`/`tarball_len` pair; version `3` added a sparse provenance section and an 8-byte footer while preserving the 36-byte package entries. On version mismatch, the reader rejects the binary file and falls back to parsing the TOML; the next write rebuilds the binary lockfile in the current version, completing the migration transparently.

## When the binary lockfile is skipped [#when-the-binary-lockfile-is-skipped]

`lpm.lockb` is intentionally smaller than `lpm.lock`. It doesn't carry every field — importer snapshots, patch records, npm-alias metadata, ambient peer installs, per-package peers, dependency engine constraints, platform metadata, optional reachability, catalogs, and registry signatures live only in TOML. Projects with any of those fields write &#x2A;*only `lpm.lock`**. Verified provenance is representable in v3, but importer snapshots normally keep install-produced lockfiles TOML-only.

Why: silently dropping metadata would produce a binary lockfile that disagrees with the reviewer-visible TOML lockfile. Skipping the write and removing stale binary state is the safe behavior.

If your project only has `lpm.lock`, that's expected when the graph needs TOML-only metadata.

## Determinism [#determinism]

The lockfile is deterministic by construction:

* Entries sorted by name
* Per-entry `dependencies` arrays sorted
* Optional fields omitted (never written as `null`)
* Schema-versioned

This is what makes `git diff lpm.lock` actionable. A new dep adds entries. A version bump rewrites a single package's fields. Supply-chain surprises (a transitive integrity change without a version bump, a new package appearing without a corresponding `package.json` change) are visible without parsing.

## Should I commit `lpm.lockb`? [#should-i-commit-lpmlockb]

Commit `lpm.lock` always. Commit `lpm.lockb` when LPM CLI wrote it. If the project is TOML-only because the graph carries metadata outside the binary subset, do not synthesize a binary file by hand.

The only special handling: mark `lpm.lockb` as binary in `.gitattributes`:

```text
lpm.lockb binary
```

`lpm init` adds this line for you.

## See also [#see-also]

* [`lpm install`](/docs/packages/install) — what reads the lockfile, and how `--offline` / `--strict-integrity` work
* [`lpm.lock` format](/docs/reference/lockfile-format) — full TOML schema reference
* [`lpm.lockb` format](/docs/reference/lockb-format) — binary format wire spec
* [Resolver](/docs/packages/resolver) — what produces a lockfile
* [Content-addressable store](/docs/packages/content-addressable-store) — where the bytes the lockfile points at live
