palm.yaml and palm.lock.yaml
A project that uses palm commits three things: palm.yaml, palm.lock.yaml and the files palm generated.
palm.yaml says what you want. The lock says what that resolved to and what palm wrote. git carries the files to every clone and every CI run.
$ 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: marketplaceentries: - kind: skill name: tdd source: mattpocock/skills path: skills/engineering/tdd content: sha256:e751ad1a5bc7e0600cfc571ddb396bcec66547a02c03ce1d3b8b5bc6d5a2069e render: {claude: sha256:04476c5e20ef4f78bac75223193a01ac7248dfc993dce7d085160e577436dec8, codex: sha256:c3bb4f3b844914d883e9628da120269fb927a582796e4dce255fc07d4e01d1f7} files: - .agents/skills/tdd/SKILL.md - .agents/skills/tdd/agents/openai.yaml - .agents/skills/tdd/references/mocking.md - .claude/skills/tdd/SKILL.md - .claude/skills/tdd/references/mocking.md notes: - claude gets its own copy in .claude/skills$ palm install1 unchanged.$ palm check✓ manifest and lock agree✓ palm.yaml targets match the lock✓ every entry is rendered✓ every entry is on all its targets✓ generated files match the lock✓ no stray file in a folder palm owns- applied files outside the lock: skipped (global scope only)✓ in-repo sources match the lock✓ no generated path lies inside a source✓ every program palm installed is trusted✓ no foreign command in the hook files palm manages✓ no foreign stdio server in the MCP files palm manages✓ every hook script exists✓ no literal secret in generated files✓ every variable the servers need is set✓ generated files are committed✓ every source is declared✓ no link leaves the scope✓ no hidden Unicode in generated files✓ no harness loads an entity twice✓ no two agents or commands share a name in a harness folder✓ every skill an agent preloads is installed✓ instruction files are within harness limitsno problemsThe lock pins the source to a commit and records, per entry, one content hash, one render hash per harness and the file list.
A bare palm install on a current checkout writes nothing, and palm check confirms that every file matches.
Three files, three jobs
Section titled “Three files, three jobs”| File | Holds | Written by | Commit it |
|---|---|---|---|
palm.yaml |
targets; sources with ref:, root: and layout:; entries with filters; hand-declared MCP servers |
palm init, palm install, palm remove, palm update --to, your editor |
yes |
palm.lock.yaml |
per source the URL, range, resolved tag, commit and layout; per entry the content hash, one render hash per target, the files, merged-entry identities, executable hashes, trust and notes | palm install, palm remove, palm update |
yes |
| generated files | every harness file palm wrote, and .palm/assets/ |
palm install, palm update, palm remove |
yes |
.palm/local/ and palm.local.yaml are the only palm paths git ignores. They are reserved for personal additions in palm 0.3.
Why the generated files are committed
Section titled “Why the generated files are committed”Every generated file is a function of palm.yaml, the lock and the sources.
palm could rebuild them on each machine, but committing them has three payoffs.
- A clone works without palm. Harnesses read committed files, and hook scripts sit in
.palm/assets/, so nothing points at a missing path. - The diff is the review. An update shows the changed skill text, the new hook command and the moved lock hash in one pull request.
- git is the record of the machine. palm keeps no state file in the project, and a teammate’s
git pulldelivers a removal as a deleted file.
.palm/assets/ holds scripts a reviewer should read, so palm does not mark it as generated for GitHub’s diff view.
A bare install syncs
Section titled “A bare install syncs”palm install with no arguments makes the disk match palm.yaml and the lock.
It compares three things for each entry: what palm.yaml asks for, what the lock records, and what is on disk.
| Situation | What palm does | Status |
|---|---|---|
In palm.yaml, not in the lock |
resolves, renders, writes, adds it to the lock | + installed |
In the lock, not in palm.yaml |
deletes its files and merged entries, drops it from the lock | - removed |
| In both, files match the render | nothing | one unchanged count line for all of them |
| A generated file is missing | writes it again from the locked commit | ↺ restored |
| A generated file differs from the render | keeps your file, exits 1 | ! modified (kept) |
| The render changed, such as an edited in-repo source | writes the new render, updates the lock | ~ re-rendered |
ref: in palm.yaml differs from the lock |
prints the update plan, asks for consent when a program changes, then resolves the new ref | ~ updated |
A bare install needs no network when the cache holds every locked commit.
On a clean clone with a current lock, it writes neither palm.yaml nor the lock.
--dry-run prints would install, would restore and would remove lines and writes nothing.
A run that installs nothing writes nothing.
When no entity installs, because it failed, was refused or was declined, palm.yaml and the lock keep their bytes.
Edits are detected by hash
Section titled “Edits are detected by hash”The lock stores one render hash per entry and target, computed from the files and merged entries palm would write. Every machine computes the same hash from the same lock, so a teammate’s pull that moves the lock is an upgrade, never “your edits”.
A file that differs from the render is yours.
palm never overwrites it or deletes it without --force, and the message names the command that does.
! kept .claude/skills/tdd/SKILL.md: you changed it since palm wrote it (palm remove mattpocock/skills skill:tdd --force)What the lock never holds
Section titled “What the lock never holds”The lock holds no absolute path, home directory, hostname, timestamp, per-file hash or secret value.
A newcomer’s lock for the same palm.yaml is byte for byte the same as a veteran’s.
Entries are sorted by kind and name, and saving the same lock twice gives the same bytes.
Merged entries, such as a hook inside .claude/settings.json, carry an identity independent of their value.
An edited hook is “changed”, not “missing”, and removing one plugin can never take another plugin’s hook.
palm writes palm.yaml with targets: first, and switches a list longer than three entries to block style, one entry per line.