Skip to content

axiom verify --tree and the GitHub Action

A manifest says what a tree should contain at the paths it touched. verify --tree asks whether a real tree agrees — in CI, before a merge, long after the agent that produced the manifest is gone.

axiom verify <bundle.json> --tree <root> [--pre] [--attest <out.intoto.json>]
flag meaning
--tree <root> compare the tree under root with the manifest
--pre compare with the manifest’s declared pre-image set (ManifestBody.preImage, S-402) instead of the post-apply artifacts — “is this the tree the manifest was compiled against?”
--attest <file> on success, write an in-toto Statement to <file> and the bare predicate to <file minus .intoto.json>.predicate.json (both JCS-canonical)

Exit 0 when every path matches, 1 on any mismatch (or when --pre is asked of a manifest that carries no preImage), 2 on usage / structural failure. Output is JSON:

{
"ok": false,
"manifestDigest": "sha256:…",
"canonical": true,
"root": "/home/ci/repo",
"tree": "post",
"paths": 2,
"mismatches": [ { "path": "src/x.ts", "op": "create", "expected": "<sha256>", "actual": "<sha256>" } ]
}

Only the manifest’s own paths are compared: every create/overwrite artifact must be present with the declared digest, every delete must be absent. Files the manifest never mentioned are ignored, so the check survives unrelated commits. A whole-root digest was rejected for exactly that reason. --pre swaps the expected set for preImage[], which lists what compile saw for each path (digest or absent).

A symlink, directory or containment escape where a file is expected is a mismatch whose actual is the error code (ERR_SYMLINK_IN_PATH, non-file:dir, …). The command never takes the lock and never writes under .axiom/; structural verification (verifyBundle) runs first, so a self-inconsistent bundle stops before any file is read.

--attest emits an in-toto Statement v1 with

  • predicateType: https://axiom.dev/attestation/apply/v1
  • subject: the manifest (name = plan name, digest = manifestDigest) plus one entry per present artifact path, so gh attestation verify can be pointed at either the manifest digest or a single file.
  • predicate: { manifest, plan, profile, tree: "post"|"pre", paths[], preImage?[], verifier: { id, version }, source? }source carries GITHUB_REPOSITORY/REF/SHA/RUN_ID when present, nothing else. No clock: the same inputs produce identical bytes.

The Statement is what you keep; the sibling .predicate.json is what actions/attest wants (it builds the Statement itself from predicate-path + the subject you name).

Inputs, outputs and permissions in full: integration/github-action.md.

.github/workflows/axiom-verify.yml
on: pull_request
permissions:
contents: read
id-token: write # only for attest: true
attestations: write # only for attest: true
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: dragoscv/axiom/action@v2
with:
bundle: .axiom/manifests/<hex>.json # the bundle the agent applied
root: .
# pre: true # verify the pre-image set instead
# attest: true # upload an attestation on success
input default meaning
bundle ManifestBundle JSON
root . tree to verify
pre false verify preImage instead of artifacts
attest false write + upload the in-toto attestation via actions/attest@v4
attestation-path axiom-attestation.intoto.json where the Statement is written
version 2 @codai/axiom-mcp dist-tag / version installed with npm i -g
local-cli path to a built cli.js (dogfooding only)

Outputs: ok, manifest-digest, mismatches (count), attestation-path. A mismatch fails the step with an ::error annotation naming the digest and the count; the JSON is in the log.

Verify later. The subject is the manifest digest (sha256(JCS(manifest))), not the sha256 of any file on disk, so gh attestation verify <file> — which hashes the file you pass — cannot be pointed at bundle.json. Look the attestation up by subject digest instead:

Terminal window
# the digest is in .axiom/applied/<hex>.json, a CheckReport, or `axiom verify` output
gh api repos/<owner>/<repo>/attestations/sha256:<manifest hex> \
--jq '.attestations[].bundle.dsseEnvelope.payload' | base64 -d \
| jq '.predicateType, .subject, .predicate.tree, .predicate.source'

To run the full Sigstore verification with gh, give it a file whose sha256 is the manifest digest — the JCS bytes of bundle.manifest (that is what the digest hashes):

Terminal window
node -e 'const {canonicalize}=require("@codai/axiom-canon");const b=require(process.argv[1]);process.stdout.write(canonicalize(b.manifest))' bundle.json > manifest.jcs
gh attestation verify manifest.jcs --repo <owner>/<repo> --predicate-type https://axiom.dev/attestation/apply/v1

gh fetches the bundle for that digest from GitHub, checks the Sigstore signature, the certificate’s workflow identity and the Rekor entry, and prints the verified statement.

The first CI run that exercised this (dragoscv/axiom run 35476782913, 2026-09-20) produced one attestation for ci-verify@sha256:b1df83a6…, signed by the public Sigstore instance (logIndex 2893697943), with predicate.source = { repository, ref, sha, runId } — fetched and decoded with the command above.

The repository’s own CI runs the action against a scratch tree on every push (verify-action job): it must pass on the applied tree, fail with one mismatch after a hand edit, and upload an attestation on main.


See also

  • GitHub Action — every input/output, the @v2 moving tag
  • Signing — DSSE signatures on the bundle itself (a different proof)
  • Trust model — attestation among the other trust boundaries
  • Plan formatpreImage[], which --pre compares against