After this chapter you can read apm.lock.yaml and say exactly what it guarantees —
the three levels of identity it records (resolved_ref → resolved_commit
→ content_hash), why bare apm install reproduces the same bytes instead
of chasing whatever main points at today, and how to turn that guarantee into a
fail-closed CI gate with apm install --frozen. You will also use apm lock to
write the contract without deploying, and apm find to trace any deployed file
back to the package that produced it. Chapter 5 made the
lockfile appear as a by-product; here it becomes the subject — the moment
Reproducibility stops being a promise and
becomes a mechanism.
Objective
Concept/Theory
A manifest resolves; a lockfile freezes
Every install in Chapter 5 quietly wrote
apm.lock.yaml, and you were told to notice two fields inside it —
resolved_commit and content_hash — without yet studying them. This is
the chapter that opens that file up, because it answers the question Chapter 5 deliberately deferred:
why does re-running apm install reproduce the same bytes instead of fetching whatever
is newest?
The answer is a distinction. A manifest declares intent, and intent is often a moving
target: a dependency's ref can be a branch (main), a tag (v1.2.0), a commit
SHA, or a semver range (^1.2.0). A branch pointer moves with every upstream push; a range
admits whatever tag is newest the moment you run install. So two developers installing the
same apm.yml a week apart — or a laptop and a CI runner installing it the
same minute — can resolve different bytes. That is the agent-context version of
“works on my machine.”
The lockfile records the resolution: the exact commit each declared dependency
resolved to, for the whole transitive graph, plus a content fingerprint of the bytes that landed.
Where the manifest says “give me something matching this,” the lockfile says “here is
exactly what you got.” Its very first job, per the spec, is reproducibility: “apm
install --frozen reinstalls the exact commits recorded here — no resolution, no network
drift”
(Lockfile
specification). Subsequent installs replay the lockfile rather than re-resolving —
which is why restore is deterministic, and why the manifest, not the lockfile, is the thing you edit by
hand.
Three levels of identity
The heart of the chapter is that each lockfile entry pins identity at three levels, and keeping them straight is what makes “the same skill” a falsifiable claim rather than a hope:
| Level | Field | The question it answers |
|---|---|---|
| Intent | resolved_ref |
What did you ask for? — the ref from apm.yml (v1.0.0, main, a SHA) |
| Resolution | resolved_commit |
What did it resolve to? — the exact 40-char commit SHA. The pin. |
| Content | content_hash |
What are the bytes, exactly? — a SHA-256 fingerprint of the source tree |
A commit SHA answers where the content came from; a content hash answers what the content
is. Agent context is executable — a prompt is a program for a model — so “the
same review prompt” has to mean the same characters, not merely the same source pointer.
That is why the lockfile records both the pin and the hash: “content_hash
— SHA-256 of the package file tree — makes ‘same install on every clone’ mean
byte-for-byte the same”
(The three
promises). The hash is also the layer that
Security and provenance rest on: if the recorded hash
and the on-disk bytes ever diverge, something changed — full stop.
The hash follows the bytes, not the ref
Pinning is by commit; integrity is by content — and the two can move
independently. Meridian's one transitive dependency makes this concrete. In
Chapter 5 the direct dependency was pinned
(microsoft/apm-sample-package#v1.0.0), but it pulled in a skill from
github/awesome-copilot that resolved unpinned — APM even warned
[!] 1 dependency unpinned. Because #main floats, its
resolved_commit moved between sessions (from a4aebcd4… in Chapter 5 to
3169734b… here). Yet its content_hash stayed
byte-identical (sha256:9236d06a…): the ref floated onto a new
commit whose bytes were the same, so the fingerprint did not budge. That is content-addressing —
and it is also the warning behind the unpinned notice. Pinning freezes the commit; if you want
the commit frozen too, pin the ref. Deliberately moving to a newer commit is the drift door
Chapter 7 opens with apm update.
Generated, never hand-edited
apm.lock.yaml is a build product of resolution, not an authored file. You
edit apm.yml; APM writes the lockfile. That one rule is what keeps it trustworthy: its
entire value is that it is a faithful, mechanical record of what resolution produced, so the review
story lives in the diff. Change the manifest, regenerate, and the lockfile diff shows exactly
which commits and hashes moved as a consequence — a dependency change you review like any other.
And reproducibility is not staleness: the lockfile guarantees the same bytes on replay;
moving to newer bytes is a separate, consent-gated act (apm update,
Chapter 7).
In APM
Inside apm.lock.yaml
Here is the actual lockfile apm install wrote for Meridian v0.2.0, annotated field by
field. Read it top to bottom: a small header, then a flat dependencies list holding
both direct and transitive packages, then a block for your project's own materialized files.
Every dependency pins by resolved_commit and fingerprints by content_hash.
apm install --target copilot,claude,cursor generated for Meridian v0.2.0,
annotated. Repetitive path/hash lines are elided (…) for readability; every
distinct field is shown. Verified on apm v0.23.1.
lockfile_version: '1' # schema version of the lockfile FORMAT itself
generated_at: '2026-07-02T00:53:23...+00:00' # UTC time the RESOLVED CONTENT last changed; stable across restores
apm_version: 0.23.1 # the CLI that wrote this lock (provenance / debugging)
dependencies: # every resolved package: DIRECT and TRANSITIVE, one flat list
- repo_url: microsoft/apm-sample-package # the source repo (owner/repo shorthand)
name: apm-sample-package
host: github.com
resolved_ref: v1.0.0 # INTENT: the ref you pinned in apm.yml
resolved_commit: fb2851683be0e0e7711421d518bd8dba23b0b1f6 # RESOLUTION: the exact 40-char SHA -- the pin
version: 1.0.0 # the package's declared semver (from its own apm.yml)
package_type: apm_package # kind of package; drives how it deploys
deployed_files: # DEPLOYMENT RECORD: every file this package materialized
- .github/agents/design-reviewer.agent.md
- .github/instructions/design-standards.instructions.md
- .claude/agents/design-reviewer.md
- .cursor/agents/design-reviewer.md
- .agents/skills/style-checker/SKILL.md
# ... 11 more deployed paths across .claude/ .cursor/ .github/ ...
deployed_file_hashes: # CONTENT: SHA-256 of each deployed file -- what `apm audit` checks
.github/agents/design-reviewer.agent.md: sha256:616988766e... # one source primitive ->
.claude/agents/design-reviewer.md: sha256:616988766e... # identical bytes in every harness
.agents/skills/style-checker/SKILL.md: sha256:1142700284...
# ... 11 more path -> sha256 entries ...
content_hash: sha256:744cca54cc8ff7ca90aa1dd621c2f98c6291cd793815afe8518001cc94b8aba9 # fingerprint of the SOURCE tree
- repo_url: github/awesome-copilot # the TRANSITIVE dep (pulled in by the package above)
name: awesome-copilot
host: github.com
resolved_commit: 3169734bc2fb25d5e092130fc93d24b0dee3ac3a # FLOATS: unpinned #main -- do not treat as fixed
version: unknown # unpinned: no tag resolved (hence the [!] warning at install)
virtual_path: skills/review-and-refactor # the sub-path carved from the monorepo (one primitive, not a whole pkg)
is_virtual: true # a "virtual" package: a slice of a repo
depth: 2 # 1 = direct from your apm.yml; 2 = transitive
resolved_by: microsoft/apm-sample-package # PROVENANCE: the parent edge that introduced this dep
package_type: claude_skill
deployed_files:
- .agents/skills/review-and-refactor/SKILL.md
- .claude/skills/review-and-refactor/SKILL.md
content_hash: sha256:9236d06a1500089ddb46975b866e9a63478e502afe7095b1980c618678a7c7fe # IDENTICAL to Ch5 though the commit moved
local_deployed_files: # your project's OWN .apm/ sources, materialized (no dependency involved)
- .github/instructions/meridian-checkout.instructions.md
- .github/prompts/checkout-review.prompt.md
- .claude/rules/meridian-checkout.md
# ... 3 more local paths across .claude/ .cursor/ ...
local_deployed_file_hashes: # SHA-256 of each local file (environment-specific: line endings)
.github/instructions/meridian-checkout.instructions.md: sha256:bf0a9567...
# ... 5 more local path -> sha256 entries ...
A few things are worth naming. The deployed_files / deployed_file_hashes pair
is the deployment record — the map apm audit checks against disk;
notice the two design-reviewer entries share one hash (616988766e…),
proof that “one source primitive → many harness files” is still one set of
bytes. The transitive entry adds three provenance fields — is_virtual (a slice of a
repo, not a whole package), depth (2 = transitive), and
resolved_by (the edge that pulled it in). The local_deployed_* block is your
own .apm/ content; its hashes are environment-specific (line endings), so treat them as
local, not as a cross-machine assertion. And there is no mcp: section, because
this project declares mcp: [] — how MCP servers appear in the lockfile is
Chapter 8's story, not something to infer here.
One field deserves a caveat: generated_at stamps “the time the resolved content last
changed,” not “when you last ran install.” It advances only when resolution
changes, and is preserved byte-for-byte across restores — which is exactly what makes restore
provably deterministic:
apm install runs on the unchanged project leave the
lockfile byte-identical — generated_at does not even advance. Cache-served,
offline. apm v0.23.1
$ apm install ; apm install # restore twice, no changes
RUN1 generated_at: 2026-07-02T00:58:03...+00:00 sha=EAD3EE9B...747D
RUN2 generated_at: 2026-07-02T00:58:03...+00:00 sha=EAD3EE9B...747D # <- identical
BYTE_STABLE = True # generated_at frozen; file unchanged
That is the headline made mechanical: git clone + apm install lays down the
same bytes on any machine, and re-running install is a byte-stable no-op. Reproducibility, proven each
time you restore.
apm install --frozen — the fail-closed CI gate
Reproducibility is only real if something enforces it. A regular install is forgiving —
if apm.yml gained a dependency that is not locked yet, it will resolve and lock it for you.
That convenience is exactly wrong in CI, where a passing build must prove that what is committed
is internally consistent. apm install --frozen makes the lockfile a
contract: it “mirrors npm ci” and refuses to install when the
lockfile is missing or has drifted from the manifest
(apm install).
Critically, it is a structural presence check, not a byte comparator. On an in-sync
project it behaves like a normal restore and ends with a tell that names its own boundary:
apm install --frozen on the in-sync project: a normal restore that ends with the
--frozen tell — it verified presence and points you at
apm audit for on-disk content. Offline (cache-served), exit 0.
apm v0.23.1
$ apm install --frozen
[i] Targets: claude, copilot, cursor (source: apm.yml)
[+] microsoft/apm-sample-package #v1.0.0 @fb285168 (cached) |-- adopted
[+] github.com/.../review-and-refactor @3169734b (cached) |-- (files unchanged)
[+] <project root> (local) |-- adopted
[*] Installed 2 APM dependencies in 13.0s.
[i] Lockfile presence verified. Run 'apm audit' for on-disk content integrity. # <- the --frozen tell
That last line is the load-bearing nuance of the whole chapter. --frozen guarantees the
graph is the locked graph — the lockfile exists and the manifest agrees with it. It does
not re-hash your deployed files. Here is precisely what does and does not trip it, all
verified on v0.23.1:
| Situation | --frozen |
Why |
|---|---|---|
| Lockfile missing entirely | FAIL (exit 1) |
nothing to reproduce from |
A package declared in apm.yml is absent from the lock |
FAIL (exit 1) |
manifest ↔ lockfile disagree — the real CI break |
Change a pinned ref to another ref at the same commit (#v1.0.0 → #main) |
PASS | the package is still present; the ref string is not what --frozen guards |
| Add a dependency whose repo is already locked (even transitively) | PASS | the repo is present; it is promoted, not “missing” |
| A floating ref drifts to a different commit | PASS (not caught) | structural, not a commit comparator — the resolved_commit pin holds this |
| A deployed file is hand-edited on disk | PASS (not caught) | structural, not content — that is apm audit's job |
So the running-example break is not “a ref moved upstream.” It is a package that is
declared in the manifest but missing from the committed lockfile — someone added
a dependency and did not commit the re-locked file. The docs are explicit about the boundary:
“This is a structural check, not a content check — run apm audit --ci for hash
verification”
(apm install).
Pair them and never conflate them: --frozen = the graph is the locked graph;
apm audit = the bytes are the locked bytes (the audit command lands in
Chapter 7, and Chapter 8 builds
its security guarantees on the same content hash).
apm lock — write the contract without deploying
Sometimes you want the lockfile refreshed but you do not want to touch your working tree —
a PR check that only asks “does this manifest resolve cleanly?”, or a headless job that
regenerates the lock, or an SBOM export. apm lock is that tool: it “resolves
dependencies and writes apm.lock.yaml without deploying files”
(apm lock). We
proved the no-deploy guarantee by deleting the lockfile and one already-deployed file as a
canary, then running apm lock:
apm lock resolves the graph and writes the lockfile but deploys nothing
— a deleted deployed file (the canary) is not recreated, and the lock it writes omits
the deployment record. needs network
apm v0.23.1
$ del apm.lock.yaml
$ del .github/instructions/design-standards.instructions.md # canary: an already-deployed file
$ apm lock
[+] microsoft/apm-sample-package #v1.0.0 @fb285168
[+] github.com/github/awesome-copilot/skills/review-and-refactor #default @3169734b
[+] Lockfile written to apm.lock.yaml
# lockfile regenerated? True canary recreated? False # <- resolved + locked, but NOT deployed
# the new lock has NO deployed_files / local_deployed_files # <- resolution-only
That last comment is the catch: an apm lock file is resolution-only. It
records resolved_commit / resolved_ref / content_hash per
dependency but omits deployed_files, deployed_file_hashes, and the
local_deployed_* block — because it never deployed. It is perfect for validation and
apm lock export (SBOM/inventory), but it is not the same artifact apm install
commits: a later install will add the deployment record and change the file. Use apm lock
to compute the contract; use apm install to compute and materialize it.
apm find — trace any file to its origin
Reproducibility is worthless if you cannot answer “where did this file come from?”
apm find closes that loop. Reading straight from apm.lock.yaml, it prints the
package that owns a deployed file; --source appends the resolved coordinate, and
--path prints the full root-to-target chain, including the transitive edge:
apm find traces a deployed file to the package that produced it, straight from the
lockfile. --source adds the resolved coordinate; --path prints the full
provenance chain, including the transitive edge. The awesome-copilot@3169734b
coordinate is unpinned — the tip at capture time; #main floats, so your run may
differ. Offline.
apm v0.23.1
$ apm find .github/instructions/design-standards.instructions.md
microsoft/apm-sample-package # the owning package
$ apm find .github/instructions/design-standards.instructions.md --source
microsoft/apm-sample-package microsoft/apm-sample-package@v1.0.0 # pinned -> @tag
$ apm find .agents/skills/review-and-refactor/SKILL.md --path
github/awesome-copilot
apm.yml -> microsoft/apm-sample-package -> github/awesome-copilot # the transitive chain
$ apm find .agents/skills/review-and-refactor/SKILL.md --source
github/awesome-copilot github/awesome-copilot@3169734bc2fb # unpinned -> @commit
$ apm find .github/instructions/meridian-checkout.instructions.md --source
. (workspace) # your own .apm/ source, not a package
$ apm find .github/does-not-exist.md
[x] '.github/does-not-exist.md' is not tracked by any installed package in apm.lock.yaml. # exit 1
Notice how the coordinate mirrors the pin: a pinned dependency resolves to pkg@v1.0.0
(the tag), an unpinned transitive one to pkg@<commit>, and a local file to
. (workspace). Because apm find reads the lockfile, it is only ever as honest
as the committed lock — one more reason the lockfile is the artifact this whole chapter is about.
When to use / pitfalls
Treat the lockfile as a contract
Four moves read or write that contract; reach for them by intent. Bare restore reproduces; the other three verify, regenerate, or trace.
| You want to… | Run… | Because |
|---|---|---|
| set up or re-materialize a cloned repo | apm install (bare) |
restore replays the locked graph byte-for-byte (Chapter 5) |
| gate CI on reproducibility | apm install --frozen |
fails closed if apm.yml and apm.lock.yaml disagree |
| regenerate/review the lock without touching your tree | apm lock |
resolves and writes the lock, deploys nothing (PR checks, SBOM) |
| find where a deployed file came from | apm find <file> |
traces it to the owning package and its provenance chain |
| move to newer versions | apm update (Chapter 7) |
reproducibility is not staleness; version moves are deliberate |
Worked example
The CI break, mechanically
Meridian shipped v0.2.0 in Chapter 5 with one
pinned dependency and one review script. Now Priya adds a second, branch-pinned
dependency. On her laptop apm install resolves it and updates apm.lock.yaml
— but she commits only the manifest edit:
apm.yml change Priya committed — a new branch-pinned dependency
(acme/checkout-guardrails is a throwaway illustration for this CI-break scenario, not a
dependency Meridian adds to its canonical manifest). The regenerated lockfile was
not committed alongside it. apm v0.23.1
dependencies:
apm:
- microsoft/apm-sample-package#v1.0.0
- acme/checkout-guardrails#main # <- added, branch-pinned; lock re-generated locally but NOT committed
mcp: []
CI checks out that commit — new manifest, old lockfile — and runs the reproducibility gate. It fails closed:
apm install --frozen and fails: the manifest names a package the committed
lockfile does not pin. Exit 1 in 0.1s, with no network access
— the structural gate fires before any clone is attempted.
apm v0.23.1
$ apm install --frozen
[>] Installing dependencies from apm.yml...
--frozen: apm.lock.yaml is out of sync with apm.yml.
- acme/checkout-guardrails is declared in apm.yml but missing from apm.lock.yaml
[i] Tip: run 'apm outdated' to see what changed, then 'apm update'.
[!] Install interrupted after 0.1s. # <- exit 1, no network, nothing written
Read the message literally: the break is a manifest ↔ lockfile mismatch, not an
upstream ref that moved. The gate never even tried to clone acme/checkout-guardrails
— it fired in a tenth of a second, before resolution, precisely because the package is
declared but not locked. (That the illustrative repo does not exist is irrelevant: a real
forgotten re-lock produces the identical error, because --frozen is checking presence, not
fetching bytes.)
The fix is the discipline the whole chapter argues for. Priya does not hand-edit the lockfile.
She reconciles by running plain apm install, which resolves the new dependency, regenerates
apm.lock.yaml with its pin and fingerprint, and deploys it. The regenerated lockfile's diff
is what the pull request reviews — a dependency change, read like any other:
resolved_commit (the pin) and content_hash (the fingerprint). Illustrative:
the exact SHA and hash depend on the package. apm v0.23.1
dependencies:
- repo_url: microsoft/apm-sample-package
resolved_ref: v1.0.0
resolved_commit: fb2851683be0e0e7711421d518bd8dba23b0b1f6
content_hash: sha256:744cca54...
+ - repo_url: acme/checkout-guardrails # the new dependency, now pinned
+ resolved_ref: main
+ resolved_commit: <40-char SHA that #main resolved to>
+ content_hash: sha256:<fingerprint of the deployed bytes>
She commits the lockfile, and CI re-runs the gate. With manifest and lockfile in agreement again,
--frozen passes and prints its tell:
0. apm v0.23.1
$ apm install --frozen
...
[i] Lockfile presence verified. Run 'apm audit' for on-disk content integrity. # exit 0
Nothing exotic happened: a manifest edit outran its own committed lockfile, the gate caught it before touching the network, and a regenerate-review-commit brought the two back into agreement. The team leaves with a rule they now treat as non-negotiable — the lockfile is the reproducibility contract: generated, reviewed in the diff, and committed alongside the manifest that produced it. That is Reproducibility, enforced.
Recap & next
Recap
-
The manifest names intent; the lockfile freezes the fact.
apm.lock.yamlrecords the exact resolved graph — direct and transitive — so restore is byte-for-byte. This is where Reproducibility becomes a mechanism, not a promise. -
Three levels of identity:
resolved_ref(what you asked for) →resolved_commit(the pin) →content_hash(the exact bytes). The hash follows the bytes, not the ref — a floated commit with identical content keeps an identical fingerprint. -
apm install --frozenis a structural presence gate, not a content check and not a ref comparator. It fails when the lockfile is missing or when a package is declared inapm.ymlbut absent from the lock — the real CI break. On-disk byte integrity isapm audit's job, kept deliberately distinct. -
apm lockwrites the contract without deploying (resolution-only; omits the deployment record) — ideal for PR checks and SBOMs.apm findtraces any deployed file to its owning package and provenance chain. -
Never hand-edit the lockfile. Fix the manifest, regenerate, review the diff like a
dependency change, and commit the re-locked file. Restore is byte-stable — a no-op install even
leaves
generated_atunchanged. -
Meridian's CI break was a manifest↔lockfile mismatch — a declared package
missing from the committed lock — caught offline in
0.1sand fixed by reconcile-review-commit.
Next
Reproducibility holds the graph still; the next question is how to move it deliberately.
Chapter 7 — Lifecycle opens the intentional-change door named here:
apm outdated to see what drifted, apm update to move pins forward on purpose,
and apm audit — the content-integrity companion to --frozen — to
verify that the bytes on disk still match the bytes the lockfile promised.