Skip to content

Lockfile specification

Normative reference: this page documents the v0.2 working-draft lockfile format as emitted by the current CLI. The normative, ratified contract for v0.1 is defined in OpenAPM v0.1, Section 5 (Lockfile) and published as JSON Schema at lockfile-v0.1.schema.json.

apm.lock.yaml is the pinned record of every resolved dependency and every file APM deployed into the workspace. It is the source of truth for reproducible installs and for drift detection. Commit it.

The pin covers the full dependency graph as it existed when APM resolved it, including transitive package manifests. A later upstream edit to a transitive package’s apm.yml does not change installs that replay an existing lockfile; APM keeps using the recorded commits until you run apm update, apm lock --update, or delete apm.lock.yaml and re-run apm install after changing apm.yml.

This is a Working Draft. The lock file format has two versions in use: "1" (plain Git projects) and "2" (projects with at least one registry-sourced dependency or Git semver-resolved dependency). The bump is opportunistic; see Version bumping. Registry-sourced dependencies require the experimental registries feature (apm experimental enable registries) before install or replay.

The lockfile gives APM four things:

  1. Reproducibility. apm install --frozen reinstalls the exact commits recorded here - no resolution, no network drift. Regular apm install also reuses locked commits for unchanged Git dependencies, including transitive entries, so the graph does not silently follow upstream manifest moves.
  2. Integrity. Recorded SHA-256 hashes let apm audit detect tampering with deployed files.
  3. Cleanup. The list of deployed files lets apm prune remove orphans when a dependency is dropped from apm.yml.
  4. Inspection. apm view --lock and apm audit read the lockfile to answer “what is actually installed”.

The lockfile lives at the project root next to apm.yml:

my-project/
|- apm.yml
|- apm.lock.yaml <- here
|- apm_modules/

Always commit it. The lockfile is what makes a fresh clone install identically on any machine.

lockfile_version: "1"
apm_version: "0.6.4"
dependencies:
- repo_url: https://github.com/acme-corp/security-baseline
resolved_commit: a1b2c3d4e5f6789012345678901234567890abcd
resolved_ref: v2.1.0
version: "2.1.0"
depth: 1
package_type: apm_package
deployed_files:
- .github/instructions/security.instructions.md
- .github/agents/security-auditor.agent.md
- repo_url: https://github.com/acme-corp/common-prompts
resolved_commit: f6e5d4c3b2a1098765432109876543210fedcba9
resolved_ref: main
depth: 2
resolved_by: https://github.com/acme-corp/security-baseline
package_type: apm_package
deployed_files:
- .github/instructions/common-guidelines.instructions.md
- repo_url: https://github.com/acme-corp/security-baseline
source: registry
version: "2.1.0"
resolved_url: https://registry.example.com/v1/packages/acme/security-baseline/versions/2.1.0/download
resolved_hash: "sha256:abc123..."
depth: 1
package_type: apm_package
mcp_servers:
- github
- transitive-server
mcp_configs:
github:
type: stdio
command: docker
args: ["run", "-i", "--rm", "ghcr.io/github/github-mcp-server"]
transitive-server:
type: stdio
command: local-server
mcp_target_servers:
codex:
- github
copilot:
- github
- transitive-server
mcp_config_provenance:
transitive-server: local-package
lsp_servers:
- pyright
lsp_configs:
pyright:
name: pyright
command: pyright-langserver
args: ["--stdio"]
extensionToLanguage:
".py": python
lsp_target_servers:
claude:
- pyright
lsp_config_provenance:
pyright: "project:."
local_deployed_files:
- .github/skills/my-local-skill/SKILL.md
local_deployed_file_hashes:
.github/skills/my-local-skill/SKILL.md: "a1b2c3..."
deployments:
- kind: project-relative
target: copilot
value: .github/instructions/security.instructions.md
runtime: null
scope: project
owners:
- https://github.com/acme-corp/security-baseline
active_owner: https://github.com/acme-corp/security-baseline
content_hash: "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
Field Type Required Notes
lockfile_version string yes Schema version. "1" for plain Git projects; "2" when any dependency has source: "registry" or Git semver resolution fields (constraint, resolved_tag, resolved_at).
(Deprecated) generated_at ISO 8601 string no Legacy write timestamp. New lockfiles omit it; when an existing lockfile carries it, APM refreshes it on substantive writes. Ignored by equivalence checks.
apm_version string no APM CLI version that wrote the file. Diagnostic except for a narrow compatibility path: exact APM 0.28 metadata, a locked marketplace_plugin type, and a matching content hash together authorize the receipt-less cached-plugin upgrade. It never overrides canonical apm.yml precedence or applies to freshly fetched dependencies.
dependencies list yes Resolved APM packages. See per-entry fields.
mcp_servers list of strings no Names of MCP servers managed as of the last install or update, including transitively contributed servers.
mcp_configs map no server_name -> resolved config dict baseline used to detect MCP drift.
mcp_target_servers map of string lists no target -> server names for MCP entries APM successfully wrote. Reinstall and uninstall use this ownership record to remove only APM-managed entries. An explicitly empty map authorizes no cleanup. Older lockfiles without this field adopt an existing self-defined native entry only when it exactly matches the stored mcp_configs baseline; registry-resolved and user-edited entries remain unowned.
mcp_config_provenance map no server_name -> declaring package for transitively contributed MCP servers. Used to identify the former owner in config-consistency diagnostics; it never exempts a lock-only entry.
lsp_servers list of strings no Names of all LSP servers in current APM-managed state, including package- and bundle-contributed servers.
lsp_configs map no server_name -> resolved config dict retained for owner-aware lifecycle reconciliation. It is not an apm audit drift baseline.
lsp_target_servers map of string lists no target -> server names for LSP entries APM successfully wrote. Reinstall and uninstall use this ownership record to remove stale executable entries without claiming legacy or user-owned content.
lsp_config_provenance map of strings no server_name -> declaration owner using project:., package:<identity>, or bundle:<identity>. Reconciliation uses this field to preserve bundle and surviving-package entries while removing departed owners.
local_deployed_files list no Files this project itself contributes (sources its own primitives). Reinstall reconciles these paths with the same target rules as per-dependency deployed_files. See self entry.
local_deployed_file_hashes map no path -> sha256 for local_deployed_files.
deployments list no Canonical deployment ownership rows, additive alongside the legacy deployed_files/local_deployed_files views. See Canonical deployment rows.

Each item in deployments is one locator – one deployed file or native runtime entry – with its full ownership history:

Field Type Notes
kind string Locator storage form: project-relative, target-relative, or uri (e.g. an MCP server entry).
target string Deploy target name (copilot, claude, mcp, etc.).
value string The path (relative to its kind) or URI value.
runtime string or null Runtime scoping the row, when applicable (e.g. an MCP client name).
scope string Install scope, typically project.
owners list of strings Every dependency key that has ever claimed this locator, oldest to newest.
active_owner string The current claimant. Always one of owners; a row where it is not is malformed.
content_hash string or null sha256:<hex> digest of the deployed content, when known.

owners and active_owner must each resolve to a member of the valid owner universe for that lockfile: every current key in dependencies, the workspace self-owner ., or local-bundle (imperative content added outside the replayable install pipeline, e.g. apm unpack). A reference to a dependency key no longer present in dependencies is a stale owner – apm audit reports it as deployment-ledger-owners and apm prune repairs it; see Baseline checks and apm prune.

deployments is the canonical source APM writes to going forward. The per-dependency deployed_files/deployed_file_hashes and the top-level local_deployed_files/local_deployed_file_hashes/mcp_target_servers/ lsp_target_servers fields remain on disk as derived, one-cycle-compatible legacy views of the same ledger – older tooling that only reads the flat fields still sees a consistent projection. Author neither view by hand; both are written by apm install, apm prune, and related commands.

Each item in dependencies describes one resolved package.

Field Type Required Notes
repo_url string yes Canonical repository path or URL. Entry identity is derived from repo_url, host, and virtual/local markers; see lockfile identity keys.
materialization_repo_url string no Source-cased path that preserves repository display spelling when APM reconstructs the dependency, including for apm_modules/ materialization and generated links. Omitted when it equals repo_url; it must normalize to the same identity and cannot redirect a lock entry.
host string no FQDN when not inferable from repo_url (e.g. for registry proxies or non-GitHub hosts).
host_type string no Explicit host-kind hint, currently gitlab, copied from object-form type: gitlab.
port int no Non-standard SSH/HTTPS port. Validated to 1..65535 on read.
registry_prefix string no URL path prefix when resolved through a registry proxy (e.g. artifactory/github).
resolved_ref string no The user-supplied ref from apm.yml (main, v1.2.0, a SHA).
resolved_commit string no Exact 40-char commit SHA installed. The pin.
name string no Package name as declared in the dependency’s own apm.yml at resolution time. For package_type: claude_skill, the name comes from SKILL.md frontmatter with the directory name as fallback. SELF-ASSERTED author-claim metadata – NOT integrity-verified and MUST NOT be used for trust decisions or identity keying. Always cross-reference repo_url + resolved_commit (or resolved_hash) for provenance. Omitted when absent.
version string no Resolved package version. For registry entries: the exact version selected from the registry for reinstall; resolved_hash remains the integrity anchor. For git/local entries: the version field from the dependency’s apm.yml at resolution time (display/inventory metadata only – replay always uses resolved_ref/resolved_commit). For package_type: claude_skill (which has no apm.yml), the value is always unknown. For git-semver entries: the resolved semver version. SELF-ASSERTED for git/local entries; see name boundary note above.
virtual_path string no Subpath inside the repo for virtual packages (monorepo subpaths).
is_virtual bool no true when the entry is a virtual subpath package.
depth int no Position in the dependency tree. 0 is the project itself, 1 is a direct dep, higher is transitive. Defaults to 1.
resolved_by string no repo_url of the parent that pulled this transitive dep. Absent for direct deps. Rewritten by apm uninstall when a rescued transitive dependency’s original parent is removed, so the entry stays keyed on a genuine surviving parent – this never changes the entry’s identity or lock key, only which parent it points to.
package_type string no Kind of package: apm_package, skill_bundle, claude_skill, hook_package, hybrid, marketplace_plugin. Drives target placement.
skill_subset list of strings no For dependencies that expose selectable skills: the sorted subset of skill names the manifest selected. Empty means “all”.
target_subset list of strings no Sorted target names selected by a dependency’s targets: subset. Empty means “all active install targets”.
deployed_files list of strings no Sorted project-relative paths APM wrote for this dependency; powers prune and audit‘s file-presence check. Shared paths have one canonical owner; uninstall transfers ownership to a surviving provider. Reinstall preserves other declared, gated, or dynamic targets’ entries. On explicit manifest contraction, install/prune removes dropped targets’ files only if recorded hashes match; edited files remain tracked on disk. apm lock preserves files and their deployed_files, deployed_file_hashes, and deployment-ledger rows until normal install can verify and perform cleanup. Without targets: or target: in the consumer’s apm.yml, reinstall preserves inactive targets’ deployed files, merge-hook configuration, and ownership sidecars, even with --target. A --target override does not declare dropped targets; only explicit manifest contraction permits pruning them.
deployed_file_hashes map no path -> sha256 for the files in deployed_files. Powers audit’s content-integrity check. Hashed over canonical content – UTF-8 text is normalized CRLF -> LF (bare CR preserved) so the hash is the same whether git checks the file out with Windows or POSIX line endings; binary is hashed raw. Directory entries (trailing /) have no hash.
exec_status string no Executable-trust state of this dep’s executable primitives, set by the install-time gate via the shared deny-wins resolver. One of deployed (trusted and materialized), gated_pending_approval (present but parked until approved), denied (blocked by an org/user deny), or absent (declares no executables). Consumed by audit’s required-executable-untrusted signal; see Executable approval.
source string no "local" for path dependencies, "registry" for dedicated-registry resolutions. Absent for Git deps.
resolved_url string registry only Fully-qualified download URL used to re-fetch registry archives.
resolved_hash string registry only SHA-256 digest of the registry archive bytes, verified on every install.
local_path string no Original path from apm.yml for local deps, relative to project root.
content_hash string no SHA-256 of the materialized package tree, computed from sorted relative paths and raw file bytes. For remote dependencies it verifies that downloaded or cached content still matches the lock; for local path dependencies it detects source-tree changes.
is_dev bool no true when the dep was declared under devDependencies.
discovered_via string no Marketplace name that surfaced this package (provenance).
marketplace_plugin_name string no Plugin name as listed in that marketplace.
source_url string no Canonical marketplace source URL when the package came from a hosted marketplace.json catalog.
source_digest string no sha256:<hex> digest of the hosted marketplace.json bytes used for resolution.
is_insecure bool no true when the source URL was http://.
allow_insecure bool no true when the manifest explicitly opted in to the insecure source.
constraint string git-source semver only The original semver range from apm.yml (^1.2.0, ~1.4). Present when ref: was a range; used by drift detection so a manifest range vs. a locked tag (v1.5.3) is not a false positive, and by lockfile replay to pin the resolved tag deterministically across installs.
resolved_tag string git-source semver or SHA-pin updates The concrete annotated git tag (v1.5.3, widget--v1.5.3) that satisfied constraint or justified the latest full-SHA revision-pin update.
resolved_at string git-source semver only RFC 3339 timestamp of the resolution. Surfaces “how stale is this pin?” in apm why.
declared_license string no The license the package manifest declares (license: in apm.yml, or license in a plugin.json), recorded verbatim at resolve time and syntax-validated offline against the bundled SPDX id set. An author claim, not a conclusion from LICENSE text – APM never reads the license file. Omitted when undeclared (absence means unknown; no sentinel is stored). Surfaced by apm lock export.

Fields are emitted only when set. A minimal entry is just repo_url plus resolved_commit.

Azure DevOps uses the same generic host and repo_url fields as every other Git provider. APM derives its transient organization, project, and repository coordinates when reconstructing the dependency reference; no ADO-specific fields are persisted.

Lockfile dependency keys keep github.com implicit for migration stability: existing github.com entries remain keyed as owner/repo. Local dependencies use local_path directly. Virtual dependencies append virtual_path to the base repo key. Entries for non-default hosts prefix the key with the lowercased host (host/owner/repo), so github.com/team/skills and gitea.myorg.com/team/skills can coexist without overwriting each other, and host casing cannot create duplicate keys. Registry-proxy entries keep the bare logical key because the proxy host is transport, not package identity.

GitHub and package-registry owner/repository paths are lowercased before APM derives the key. Older mixed-case GitHub entries therefore serialize with the same key as new lowercase references. Their source spelling is retained separately in materialization_repo_url, so filesystem paths and relative links do not reuse the lowercase comparison key. Repository path casing remains identity-significant for unknown git hosts because those backends may be case-sensitive.

A project that ships its own primitives (skills, agents, prompts under .github/, .claude/, etc.) records the files it deploys to its own targets under local_deployed_files and local_deployed_file_hashes at the top level.

Internally, when the lockfile is loaded, APM synthesizes a virtual dependency entry keyed by "." so that orphan detection, audit, and prune can iterate all “owned” files uniformly. This synthesized entry has:

  • repo_url: <self>
  • source: local
  • local_path: "."
  • depth: 0
  • is_dev: true
  • deployed_files and deployed_file_hashes copied from the top-level local_deployed_* fields.

The synthesized entry is not written back to YAML - the flat local_deployed_* fields remain the on-disk source of truth. Treat the self entry as an implementation detail; do not author it by hand.

The lock file uses two schema versions:

Version Triggered by Adds
"1" Default for Git-only projects. Baseline schema.
"2" Any dependency with source: "registry" or Git semver resolution fields. resolved_url, resolved_hash, and registry version; constraint, resolved_tag, and resolved_at for Git semver pins.

The bump is opportunistic: a project that never opts into a registry keeps lockfile_version: "1" forever, even on a newer client. The first registry dep added to the graph promotes the lockfile to "2"; if every registry dep is later removed, the next write demotes back to "1". Both versions are valid on-disk formats; consumers MUST handle either.

For the registry workflow this enables, see the Registries guide.

When a project is packed with apm pack, the bundled lockfile is enriched with a top-level pack: block:

pack:
format: apm # or "plugin"
target: copilot # or comma-joined list, or "all"
packed_at: "2026-05-10T20:14:00+00:00"
mapped_from: # only when cross-target path remapping happened
- .claude/skills/
bundle_files: # only for plugin bundles
skills/my-skill/SKILL.md: "a1b2..."

The pack block is read by apm unpack to verify bundle integrity and to restore correct target paths. It is stripped from project lockfiles and only appears inside packed bundles.

local_deployed_files and local_deployed_file_hashes are stripped from bundle lockfiles - they describe the packager’s own repo, which is not shipped.

Command Reads Writes
apm install existing lockfile (for --frozen and incremental reuse) full rewrite on resolution change
apm install --frozen required never writes; fails on a missing pin or MCP config/server-name drift
apm compile yes (resolution + integrity) no
apm audit yes no
apm prune yes (orphans and deployments ownership, even with nothing else to prune) yes (after removing orphans and reconciling deployments)
apm view --lock yes no
apm unpack bundle’s pack-enriched lockfile merges into project lockfile

apm install only rewrites the file when its semantic content changes (generated_at and apm_version are ignored when comparing). A no-op install leaves the file untouched. New lockfiles omit generated_at so independent dependency changes do not manufacture timestamp conflicts. If a pre-existing lockfile includes the field, APM retains it for compatibility and refreshes it only on a substantive write. To migrate a legacy lockfile manually, delete the generated_at: ... line from apm.lock.yaml once; APM will not add it back.

The lockfile is what apm audit compares the workspace against. Each baseline check maps to specific lockfile fields:

Check Backed by
deployment-ledger-owners deployments rows’ owners and active_owner vs. the valid owner universe
lockfile-exists file presence at project root
ref-consistency resolved_ref per entry vs. apm.yml
deployed-files-present deployed_files per entry (and self entry)
content-integrity deployed_file_hashes (and local_deployed_file_hashes)
skill-subset-consistency skill_subset per entry, matched against apm.yml and the resolved package tree
config-consistency mcp_configs and mcp_config_provenance
no-orphaned-packages dependencies keys vs. apm.yml

Files listed in deployed_files without a corresponding hash entry (typically directory markers ending in /) are skipped by content-integrity. Missing files are reported by deployed-files-present, not by content-integrity, so the two checks do not double-count.

Orphan detection works in two directions:

  • Orphan packages - entries in dependencies that the manifest no longer declares. apm prune removes them and their deployed_files.
  • Orphan files - files under managed target directories that no lockfile entry claims. apm prune removes them too.

apm prune is the only command that reconciles deployments rows. The valid owner universe and metadata-only repair boundary are defined in Canonical deployment rows; cleanup behavior is documented under apm prune.

lockfile_version is the schema version of the file format itself.

  • The current versions are "1" and "2"; version "2" is emitted for registry-sourced or git-semver-resolved dependencies.
  • APM additively extends entries within each version - new optional fields may appear without bumping the version. Older APM clients ignore unknown fields.
  • Breaking changes (renames, removals, semantic shifts) require bumping lockfile_version. APM refuses to operate on a lockfile whose version it does not recognize, and will instruct the user to upgrade or regenerate.

Invalid YAML, an unsupported explicit version, an empty or non-mapping root, malformed container fields, and invalid deployment rows fail closed before APM constructs lock state. Pre-versioned legacy files migrate as v1 inputs. Fix or remove other invalid files explicitly; APM does not silently replace them with an empty lockfile.

A small project with one remote APM package, one MCP server, and its own local skill:

lockfile_version: "1"
apm_version: "0.6.4"
dependencies:
- repo_url: github.com/octocat/example-skills
resolved_ref: v1.2.0
resolved_commit: 7f3c9a4d2e1b8c7f0a9e6d5c4b3a2918f7e6d5c4
version: 1.2.0
package_type: skill_bundle
depth: 1
skill_subset:
- code-review
- test-writing
deployed_files:
- .github/skills/code-review/SKILL.md
- .github/skills/test-writing/SKILL.md
deployed_file_hashes:
.github/skills/code-review/SKILL.md: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
.github/skills/test-writing/SKILL.md: "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"
mcp_servers:
- github
mcp_configs:
github:
type: stdio
command: docker
args: ["run", "-i", "--rm", "ghcr.io/github/github-mcp-server"]
local_deployed_files:
- .github/skills/my-local-skill/SKILL.md
local_deployed_file_hashes:
.github/skills/my-local-skill/SKILL.md: "fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9"