palm.lock.yaml
palm.lock.yaml records what each source resolved to and what palm wrote. palm writes it on install, remove and update, and you commit it with palm.yaml and the generated files.
$ cat palm.lock.yaml# palm lockfile: written by palm, do not edit by hand.version: 3sources: mattpocock/skills: url: https://github.com/mattpocock/skills.git ref: ^1.2 resolved: v1.2.3 sha: 2c0db9046017ad90f70ed14046665fee10b122b9 descriptor: marketplace trailofbits/skills: url: https://github.com/trailofbits/skills.git ref: ^2.1 resolved: v2.1.0 sha: 924ffa8889fde7dae2bcd6e8f43425240daafa51 descriptor: marketplaceentries: - kind: skill name: tdd source: mattpocock/skills path: skills/engineering/tdd content: sha256:e751ad1a5bc7e0600cfc571ddb396bcec66547a02c03ce1d3b8b5bc6d5a2069e render: {claude: sha256:04476c5e20ef4f78bac75223193a01ac7248dfc993dce7d085160e577436dec8, cursor: sha256:04476c5e20ef4f78bac75223193a01ac7248dfc993dce7d085160e577436dec8} files: - .claude/skills/tdd/SKILL.md - .claude/skills/tdd/references/mocking.md notes: - cursor reads .claude/skills; no second copy - kind: hook name: gh-cli source: trailofbits/skills via: plugin:gh-cli path: plugins/gh-cli/hooks/hooks.json content: sha256:3490117f0f0718f891d6ba652d4564e7fb343084f021d1a1ce34b4bff2bd759d render: {claude: sha256:fba4ddb4f71794f14c6569b9d3dd40009f07cb1a4fa320618d37e640f2143e03, cursor: sha256:cf64b9d3d0d036e334873451b7427c6670e8c2b1e5f4c9cb00dc0a8e9c6e93d6} files: - .palm/assets/trailofbits__skills/gh-cli/plugins/gh-cli/hooks/hooks.json - .palm/assets/trailofbits__skills/gh-cli/plugins/gh-cli/hooks/intercept-github-curl.sh - .palm/assets/trailofbits__skills/gh-cli/plugins/gh-cli/hooks/persist-session-id.sh merged: - {file: .claude/settings.json, at: /hooks/PreToolUse, id: palm:hook:gh-cli:1, key: sha256:b290792c, created: true} - {file: .claude/settings.json, at: /hooks/SessionStart, id: palm:hook:gh-cli:0, key: sha256:291c1125, created: true} - {file: .cursor/hooks.json, at: /hooks/preToolUse, id: palm:hook:gh-cli:1, key: sha256:ec2c0c34, created: true} - {file: .cursor/hooks.json, at: /hooks/sessionStart, id: palm:hook:gh-cli:0, key: sha256:0e246eba, created: true} exec: commands: - {id: PreToolUse//Bash, command: CLAUDE_PLUGIN_ROOT="$CLAUDE_PROJECT_DIR"/.palm/assets/trailofbits__skills/gh-cli/plugins/gh-cli bash "$CLAUDE_PROJECT_DIR"/.palm/assets/trailofbits__skills/gh-cli/plugins/gh-cli/hooks/intercept-github-curl.sh} - {id: SessionStart//-, command: CLAUDE_PLUGIN_ROOT="$CLAUDE_PROJECT_DIR"/.palm/assets/trailofbits__skills/gh-cli/plugins/gh-cli bash "$CLAUDE_PROJECT_DIR"/.palm/assets/trailofbits__skills/gh-cli/plugins/gh-cli/hooks/persist-session-id.sh} closure: {root: .palm/assets/trailofbits__skills/gh-cli, files: 3, tree: sha256:208bc9feb334e591e9ed4f6f9a86c72cb790b6d0af6abc71d8ade7e52a7d7262} hash: sha256:23759d85ebda016f23c6343d5a49064f98ce3f65bd1d68616ad0a2541ae18adb trust: [sha256:23759d85ebda016f23c6343d5a49064f98ce3f65bd1d68616ad0a2541ae18adb] - kind: plugin name: gh-cli source: trailofbits/skills path: plugins/gh-cli content: sha256:d9c283078a93fdf1cbc152cd638db9343c55162516b53fa52233c02e4a887f3a render: {} files: [] deps: - {kind: hook, name: gh-cli}The lock is deterministic. Saving the same lock twice gives the same bytes, and a newcomer’s lock for the same palm.yaml equals a veteran’s.
Top level
Section titled “Top level”| Key | Meaning |
|---|---|
version |
3. Versions 1 and 2 come from palm 0.1, and only palm migrate reads them. |
sources |
one record per source, keyed by the name in palm.yaml |
entries |
one record per installed entity, sorted by kind, then name |
Sources
Section titled “Sources”| Key | Meaning |
|---|---|
url |
the clone URL of a git source |
root |
the folder inside the repository, when set |
ref |
the intent, copied from palm.yaml when it was resolved |
resolved |
the tag a range resolved to |
sha |
the resolved commit, the one a bare install uses |
path |
an in-repo source’s project-relative path |
layout |
the layout in effect, so another machine rebuilds the same index from the commit alone |
descriptor |
the scan rule that produced the index: descriptor, apm, marketplace, plugin-manifest or convention |
An in-repo source has no commit and no source-wide hash.
Each of its entries carries a content hash over that entry’s own files, and palm check compares entry by entry.
Two pull requests that change two different skills therefore touch two different lock lines.
Entries
Section titled “Entries”| Key | Meaning |
|---|---|
kind, name |
the entity |
source |
the source name, or manifest for an MCP server declared in palm.yaml |
path |
where the entity lives in the source |
via |
plugin:<name> when a plugin selected it |
content |
sha256 of the entity’s content in the source; for an in-repo source, the signal that its files changed |
render |
one render hash per target: sha256 over the path, mode and content hash of every file the target writes, plus every fragment it merges |
files |
every path palm wrote for the entry, sorted, closure files included |
merged |
the fragments merged into shared files, as { file, at, id, key } |
exec, trust |
the entry’s programs and your consent; see below |
notes |
what install printed once: dropped fields, skipped targets, shared folders; palm describe shows them |
targets |
only when palm.yaml narrows the entry below the scope’s set; a refused target never narrows it |
at |
the placement root from palm.yaml, stored for 0.3 |
deps |
plugin entries only: the members the plugin declared |
Two hashes per entry per target, never one per file, keep the lock short and let every machine compute the same values.
Merged entries
Section titled “Merged entries”A fragment in a shared file, such as a hook in .claude/settings.json or a server in .mcp.json, carries two identities.
| Key | Meaning |
|---|---|
file |
the shared file |
at |
a JSON pointer such as /hooks/Stop, a TOML table such as /mcp_servers/docs, or block:<id> for a Markdown block |
id |
palm’s identity, palm:<kind>:<name>:<n> |
key |
what finds the fragment on disk: the object key, the block id, or a short hash of an array item’s identity |
palm tells “changed” (found, different value) from “missing” (not found). Removing one plugin therefore never takes another plugin’s hook, even when both merged into the same array.
Exec and trust
Section titled “Exec and trust”An entry with a hook or a stdio MCP server carries its programs.
exec: commands: - { id: Stop//-, command: 'bash "$CLAUDE_PROJECT_DIR"/agent-kit/hooks/quality.sh' } hash: sha256:5d41c3b0… trust: [sha256:5d41c3b0…]| Key | Meaning |
|---|---|
exec.commands |
one line per command a harness runs: <event>//<matcher or -> for hooks, stdio for a server |
exec.closure |
the scripts the commands run and the files they read, with their count and tree hash; for an in-repo source, hashed where they sit, since they run in place |
exec.hash |
the program hash over the canonical commands, events, matchers, environment keys, working directory and the closure tree |
trust |
the hashes a person consented to; palm installs silently while exec.hash is among them |
The lock diff is the team’s consent record, with readable commands and one hash line reviewed together.
An entry with a program and no trust:, or with an empty render:, fails palm check.
A hook you declined is not in the lock. It is an exclude: on the plugin entry in palm.yaml, where review sees it.
What the lock never holds
Section titled “What the lock never holds”No absolute path, home directory, hostname, timestamp, machine fact, per-file hash, created-folder list, top-level target list, secret value or merged value. The value of a merged fragment is recomputed from the render, and a secret-shaped value appears only as a redacted hash.
The global lock
Section titled “The global lock”~/.palm/palm.lock.yaml has the same shape.
Every path starts with a token: <home>, <palm>, <agents>, <claude>, <codex>, <copilot>, <cursor>, <gemini> or <opencode>.
The same lock works on machines whose harness homes differ, and ~/.palm/applied.yaml records the real paths this machine holds.