# The lock file: vibe.lock {#root}

@status:doc/work @audience:user

[p01] `vibe.lock` is the file vibe writes and you commit: the exact versions the project got. This page explains every field so you can read a diff of it in a pull request and know what changed.

## Where it lives and who writes it {#where}

[p02] There is one `vibe.lock` per workspace, at the absolute root, beside the root [manifest](../glossary/index.xml#manifest); members never have their own. `vibe install` and `vibe update` write it; `vibe clean` keeps it; nothing edits it by hand. It is the recorded decision; the dependency tree on disk is only its consequence and can always be rebuilt from it.

> [p03] Nesting is **hierarchical grouping**, not independent resolution domains. The lockfile and unified resolution always live at the *absolute root* of the workspace tree. A nested `[workspace]` provides (a) the `[workspace.versions]` matryoshka (§2.6) and (b) logical grouping of members — never its own lockfile, never its own resolution pass.
>
> <spec://org.vibevm.core/vibevm/modules/vibe-workspace/PROP-007#NESTING-PRINCIPLE>

> [p04] **Never touched — `vibe.lock`.** The lock is the recorded resolution, not derived state: keeping it is what makes `vibe clean install --offline` reproduce the exact world from the machine cache with zero network — the mvn analogy is `target/` vs the dependency resolution, and the lock sits on the resolution side.
>
> <spec://org.vibevm.core/vibevm/modules/vibe-workspace/PROP-053#CLEAN-KEEPS-THE-LOCK>

## [meta] {#meta}

[p05]
| Field | Meaning |
| --- | --- |
| `generated_by`, `generated_at` | the vibe version and the time of the last write; informational |
| `schema_version` | the lock schema; an older vibe refuses a newer schema rather than misreading it |
| `solver` | the resolver that produced the graph |
| `root_dependencies` | the coordinates the manifests asked for directly; the baseline of the freshness check |
| `language_chain` | the resolved language preference and its fallbacks, so a re-install on another machine materialises the same files |

> [p06] The freshness check itself adds no `vibe.lock` field: the lockfile *is* the baseline, and the check is a `cargo`-style **satisfiability test** of the locked versions against the current `[requires]` — see §5.1.
>
> <spec://org.vibevm.core/vibevm/modules/vibe-workspace/PROP-011#NO-NEW-FIELD>

> [p07] `[meta].language_chain` (§2.7.5) — shipped as **one** ordered field merging the preference and its fallback, not the `language` + `language_fallback` pair this section drafted.
>
> <spec://org.vibevm.core/vibevm/modules/vibe-resolver/PROP-003#LF-META-LANGUAGE>

[p08] `root_dependencies` copies the manifest's `[requires.packages]`, so the [lock file](../glossary/index.xml#lock-file) is a self-contained snapshot that needs no manifest to read. Removing a root with `vibe uninstall` drops it from both files; removing a package that is only a transitive dependency is refused with an explanation.

> [p09] **Decision.** `vibe.lock` gains `schema_version = 2` and the following record shape per package:
>
> <spec://org.vibevm.core/vibevm/modules/vibe-registry/PROP-002#LOCKFILE-V2>

> [p10] `root_dependencies` is a **mirror of `vibe.toml` `[requires].packages`** — the lockfile keeps the user's declared roots inline so it remains a self-contained snapshot of the solve state (nothing inside `vibe.lock` requires reading `vibe.toml` to interpret). The source of truth for *what the user asked for* is the manifest's `[requires]` section; the lockfile carries a copy plus the resolved transitive closure. `vibe uninstall` of a root drops the entry from both files; `vibe uninstall` of a pure transitive is rejected with an explanation.
>
> <spec://org.vibevm.core/vibevm/modules/vibe-registry/PROP-002#LF-ROOT-DEPENDENCIES>

## [[package]] {#package-entries}

[p11]
| Field | Meaning |
| --- | --- |
| `group`, `name`, `version` | the coordinate that was resolved |
| `kind` | the package's kind, for placement and filters |
| `content_hash` | the fingerprint of the package's shippable tree; the identity; verified on every fetch |
| `registry` | the name of the registry that answered, from the manifest's list |
| `source_url`, `source_ref`, `resolved_commit` | where and at what git ref the bytes were fetched this time; informational, always the canonical address even when a mirror served |
| `source_kind` | `registry`, `git`, `override` or `path`: which resolution path produced the entry; for `path` the URL field holds the member's folder relative to the root |
| `dependencies` | the resolved dependencies of this package, as exact coordinates |
| `overridden` | true when an `[[override]]` supplied the package |
| `features`, `subskills` | the active features and subskills recorded for the package |
| `files_written` | the project files this package's install wrote outside its own folder, so that uninstall can remove exactly them |
| `via_redirect` | the address of the registry stub that delegated the package elsewhere, when one was followed; absent otherwise |

> [p12] **Decision.** A package's identity is the tuple `(kind, name, version, content_hash)`. The `content_hash` is a digest over the deterministically-ordered concatenation of `(rel_path_bytes || 0x00 || file_bytes || 0x00)` for every file in the package directory, and **the value names the recipe that produced it** ([PROP-044 §4.7](../../common/PROP-044-change-native-formats.xml#machinery)): `sha256-tree/1:<hex>` is recipe 1, whose exclusion list, path normalisation and traversal order are carried as data in `formats/hash_recipes/1.toml`; the bare `sha256:<hex>` is recipe 0, the pre-recipe form, frozen verbatim in code — not configurable, because a frozen recipe that can be edited is not frozen — so that values written before recipes were named stay readable. Two hashes are comparable only **at the same recipe**; comparing across recipes answers a question nobody asked, and is never done silently. [PROP-024 §2.2](../../common/PROP-024-code-bearing-packages.xml#shippable-tree) re-scopes this to the package's **shippable tree** — its source, minus build output (`.git/`, `.vibe/`, `target/`, `node_modules/`, `.vibeignore` globs) — so a code-bearing package's identity is its source, not its build state; that exclusion lands with the code that implements it. The URL used to fetch the content is **informational** — recorded in the lockfile for debuggability, not for identity.
>
> <spec://org.vibevm.core/vibevm/modules/vibe-registry/PROP-002#IDENTITY-TUPLE>

> [p13] `source_kind` is `"registry"` for the M1.13 default, `"git"` for git-source declarations, `"override"` for `[[override]]`-resolved (existing `overridden = true` is preserved as redundant marker for back-compat). Lockfile schema bumps to v3; v2 lockfiles read transparently and migrate to v3 on next install (everything that was `overridden = true` becomes `source_kind = "override"`; everything else `"registry"`).
>
> <spec://org.vibevm.core/vibevm/modules/vibe-registry/PROP-002#SOURCE-KIND-VALUES>

> [p14] The canonical URL is always recorded as the `source_url` in the lockfile when the fetch produces a new pin. Mirror URLs do not appear there.
>
> <spec://org.vibevm.core/vibevm/modules/vibe-registry/PROP-002#MIR-CANONICAL-IN-LOCKFILE>

## Reading a diff {#reading-a-diff}

[p15] A changed `version` with a changed `content_hash` is an update. A changed `content_hash` with the same version is impossible in a healthy world: vibe refuses it at fetch time, so if you see it in a diff someone edited the file. A changed `source_url` alone is a [mirror](../glossary/index.xml#mirror) or a host migration and means nothing for the project. A new entry with `source_kind = "override"` is a patch someone applied on purpose and should carry a reason in the commit message.

> [p16] mirror-switching, host-migration, and vendoring never change the lockfile;
>
> <spec://org.vibevm.core/vibevm/modules/vibe-registry/PROP-002#EFF-LOCKFILE-STABLE>

## Edge cases and rules {#edge-cases}

[p17] An unchanged manifest against an unchanged [lock file](../glossary/index.xml#lock-file) makes `vibe install` skip the resolver entirely: the lock is the answer.

> [p18] With the freshness check, **`vibe install` becomes lockfile-respecting**: unchanged `[requires]` ⇒ the locked versions are honoured verbatim.
>
> <spec://org.vibevm.core/vibevm/modules/vibe-workspace/PROP-011#LOCKFILE-RESPECTING>

[p19] The lock file records no [registry](../glossary/index.xml#registry) [index](../glossary/index.xml#index-registry) and no mirror: reproducing it needs only the [coordinates](../glossary/index.xml#coordinate), the [fingerprints](../glossary/index.xml#fingerprint) and a source that can serve them.

[p20] `vibe why <coordinate>` explains from the lock file and the manifests why a package is in the project, or what blocked it.

[p21] The file is read strictly: a field vibe does not know is an error, not a warning, so a hand edit or a field from a newer vibe is caught at once.

