Skip to content

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:

CI: ~/project
$ 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 limits
no 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 targets
x 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 --force
  • palm.yaml, palm.lock.yaml and 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.
  1. Add the job.

    .github/workflows/agents.yml
    name: agent setup
    on: pull_request
    jobs:
    palm:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
    with:
    node-version: 22
    - run: npm install -g @paliontech/palm@0.2.0
    - run: palm check

    Pin 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 check runs the same pinned version. A palm: key in palm.yaml that names the minimum version is planned for 0.3. palm check exits 1 when any check fails. Warnings alone exit 0.

    In a repository without palm.yaml, palm check exits 1 with x 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'
  2. Make warnings fail, if you want them to.

    Terminal window
    palm check --strict

    --strict exits 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 --offline meets an empty cache and lock-disk is skipped, so cache ~/.palm/cache between 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 the ignore: list of palm.yaml, as palm check shows.

  3. Read the report when it fails.

    Each check prints one line: ✓, x, ! or skipped with 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 --quiet prints 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, with E_NETWORK or E_IO and the path, never as files that differ.

    A palm.yaml that names a file:// source outside the repository, such as a mirror on an air-gapped runner, fails with x source "kit" is file:///…, outside the project. Pass --allow-local-sources on the runners that are meant to read it: palm check --allow-local-sources.

  4. Use JSON for anything that parses the result.

    Terminal window
    palm check --json

    stdout 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.

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.

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.

CI: ~/project
$ palm install
This 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.

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.