Check the setup in CI
A teammate edits a skill in .claude/ by hand, or adds an entry to palm.yaml without running palm.
Review misses it, and the harnesses on each laptop drift apart.
One CI step, palm check, catches both, never prompts and never writes.
When you finish, CI passes on a clean branch and fails on an edited file, with the fix on the line:
$ 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 problems$ printf 'Skip the tests when in a hurry.\n' >> .claude/skills/tdd/SKILL.md
$ palm check✓ manifest and lock agree✓ palm.yaml targets match the lock✓ every entry is rendered✓ every entry is on all its targetsx 1 generated file differs from 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 limits
x skill tdd: .claude/skills/tdd/SKILL.md differs from what palm renders; fix: palm install mattpocock/skills skill:tdd --forceBefore you start
Section titled “Before you start”palm.yaml,palm.lock.yamland the generated files are committed. See Share a team baseline.- CI can reach the sources over git. Private sources need a deploy key or token in the runner, as for any git dependency.
-
Add the job.
.github/workflows/agents.yml name: agent setupon: pull_requestjobs:palm:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- uses: actions/setup-node@v4with:node-version: 22- run: npm install -g @paliontech/palm@0.2.0- run: palm checkPin the exact version, as for any tool in CI, and move it in a pull request. A newer palm may render an entity differently, and the pin keeps that change out of unrelated pull requests. Without a global install,
npx @paliontech/palm@0.2.0 checkruns the same pinned version. Apalm:key inpalm.yamlthat names the minimum version is planned for 0.3.palm checkexits 1 when any check fails. Warnings alone exit 0.In a repository without
palm.yaml,palm checkexits 1 withx no palm.yaml here, so a missing file never passes as clean. Guard the step only when the workflow is shared with repositories that do not use palm..github/workflows/agents.yml - run: '[ ! -f palm.yaml ] || palm check' -
Make warnings fail, if you want them to.
Terminal window palm check --strict--strictexits 1 on a hook command or stdio MCP server that someone added to a harness file by hand, since no lock entry explains it, and on a check that could not run. It fails when--offlinemeets an empty cache andlock-diskis skipped, so cache~/.palm/cachebetween runs or leave the network on. Other warnings, such as a variable that is not set, still exit 0. Acknowledge a warning you accept in theignore:list ofpalm.yaml, as palm check shows. -
Read the report when it fails.
Each check prints one line:
✓,x,!orskippedwith the reason. Then come the problems, one line per entity, each ending in the command that fixes it. Run that command on your machine, review the diff, and commit.palm check --quietprints only the problems and the skipped checks, so a passing run prints nothing. A fetch that fails or a cache palm cannot write is reported as that error, withE_NETWORKorE_IOand the path, never as files that differ.A
palm.yamlthat names afile://source outside the repository, such as a mirror on an air-gapped runner, fails withx source "kit" is file:///…, outside the project. Pass--allow-local-sourceson the runners that are meant to read it:palm check --allow-local-sources. -
Use JSON for anything that parses the result.
Terminal window palm check --jsonstdout holds one document shaped
{ ok, checks: [{ id, label, status, problems: [{ entity, file, message, fix }] }], warnings }. The JSON lists every problem file by file, where the terminal groups them per entity.
A pipeline from palm 0.1 that runs palm install --frozen still works for one release: the old flag prints palm check and runs it.
Replace the line with palm check.
What palm check covers
Section titled “What palm check covers”palm check reads palm.yaml, the lock, the files palm wrote, the in-repo sources, and the harness files palm merges into or parses, such as .claude/settings.json and .mcp.json.
In those harness files it reports every hook command and stdio MCP server that no lock entry explains, in every event array, as a warning that --strict turns into a failure.
It reads nothing else, so a script or agent file added by hand outside palm’s folders is for code review, not for palm check.
| Check | Fails when | Fix |
|---|---|---|
manifest-lock |
an entry of palm.yaml has no lock entry, or the reverse; a ref: differs from the lock; a plugin filter names nothing |
palm install |
targets |
targets: in palm.yaml differs from the targets the lock rendered |
palm install --dry-run lists what goes, then palm install |
render, partial |
an entry is installed for no target, or for only some of its targets | palm install |
lock-disk |
a generated file is missing or differs from the render; a merged entry is missing or changed | palm install, or palm install <source> <name> --force for an edited file |
orphans |
a stray file sits in an entity folder palm owns | palm install <source> <name> --force |
local-sources |
an entry of an in-repo source changed since the lock was written | palm install, then commit palm.lock.yaml and the files it re-rendered |
source-paths |
a path palm owns reaches into a declared source through a link | move the source, or remove the link |
exec-trusted |
a hook or stdio server has no trusted hash, or a merged command was changed on disk | palm install --allow-exec <key>=<hash> |
hook-scripts |
a hook command names a script that is missing or not executable | palm install |
secrets |
a tracked or world-readable generated file, or a harness config palm merges into, holds a literal secret; high-entropy text in code only warns | fix the source, then palm install <source> <name> --force |
git-ignored |
a generated file, a file palm merged into, or .palm/assets is ignored by git, so teammates never get it |
add the re-include lines the fix names to .gitignore |
sources-declared |
a lock source is missing from palm.yaml, or an in-repo source is missing on disk |
palm install, or restore the directory |
links |
an output path resolves outside the project, a link under an output folder dangles, or a source overlaps an output directory | move the source, or remove the link |
hidden-unicode |
a generated file holds a bidi override, a tag character or a rare variation selector | fix the source, then palm install --force |
block-size |
above the harness’s cap of AGENTS.md blocks (a warning above 24 KiB) |
narrow the entry with targets: until at: works in 0.3 |
palm check also warns, without failing, about MCP variables that are not set (variables), generated files not yet added to git (git-ignored), hook commands and stdio servers palm did not write (foreign-hooks, foreign-servers), entities a harness loads twice, also one installed in the project and with -g (double-load), two agents or a command and a skill with one name (agent-names), an in-repo entity with the placeholder description palm create wrote (local-sources) and preloaded skills nothing installs (preloads).
Under --strict the foreign-hooks and foreign-servers warnings fail the run.
palm check lists every warning.
Checks that need git are listed as skipped (not a git repository) outside a repository.
Its only network access is fetching a locked commit that is not in the cache, so a cache between runs makes it offline.
palm check compares with the lock, not with upstream. A source pinned to a commit sha passes when its branch moves on, and --strict does not change that.
Programs in CI
Section titled “Programs in CI”A trusted hook installs in CI without a flag, because its hash is in the lock.
A new or changed one stops a bare install with E_UNTRUSTED_EXEC, and the error prints the line that consents.
$ palm installThis install adds 1 program that will run on your machine.
1. hook gh-cli from trailofbits/skills (commit 924ffa8, v2.1.0, 2026-09-28) SessionStart 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 PreToolUse Bash 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 targets: claude (.claude/settings.json) scripts: 3 files, 438 B -> .palm/assets/trailofbits__skills/gh-cli/ (committed with your repo) plugins/gh-cli/hooks/persist-session-id.sh 755 50 B sha256:1033f868 plugins/gh-cli/hooks/intercept-github-curl.sh 755 98 B sha256:6d5d4536 ... 1 more (--review shows every script)
palm never runs this itself. If you say yes, its hash goes into palm.lock.yaml,so teammates and CI install it without being asked; any change asks again.x 1 program needs your consent and there is no terminal review: palm install --dry-run --review then: palm install --allow-exec hook:gh-cli@trailofbits/skills=sha256:23759d85ebda016f23c6343d5a49064f98ce3f65bd1d68616ad0a2541ae18adb$ palm install mattpocock/skills tdd-coach --json{ "error": { "code": "E_NOT_FOUND", "message": "\"tdd-coach\" is not in source mattpocock/skills; did you mean tdd?", "hint": "palm install mattpocock/skills tdd --json" }, "warnings": []}Do not paste that --allow-exec line into a pipeline.
Review the program on your machine, where palm shows the scripts, and commit the lock with its new trust: hash.
From then on CI replays it silently, and any later change fails CI again until someone reviews it.
Scripts that call palm
Section titled “Scripts that call palm”| palm would ask about | Without a terminal |
|---|---|
| running a hook or stdio MCP server | E_UNTRUSTED_EXEC, naming the --allow-exec line |
| applying an update plan | E_NON_INTERACTIVE, naming palm update ... --yes |
| cleaning the cache | E_NON_INTERACTIVE, naming palm cache clean --yes |
a secret value under --secrets literal |
E_NON_INTERACTIVE, naming --secrets env-ref |
--yes accepts every prompt except consent.
With --json, stdout holds exactly one JSON document, and an error is { "error": { "code", "message", "hint" }, "warnings": [] }.
palm update --dry-run --strict exits 1 when a source is behind its ref, and prints why, which makes it a scheduled outdated check.
A sha pin is never behind its ref, so that check passes when the branch moves. Read the main is <sha> column of palm update --dry-run, or pin a branch or a range instead, as palm update explains.
Exit codes and errors lists every code.