Trust model
AXIOM assumes the agent is capable but not trusted: it may be wrong, it may be prompt-injected, and the harness it runs in may fail open. The design therefore moves every decision that matters out of the agent’s hands and into artefacts a human, a policy or a CI job can inspect — and makes “could not decide” mean “no”.
flowchart TB
subgraph agent [Untrusted: the agent]
AG[LLM + harness tools]
end
subgraph gate [AXIOM process — trusted code, untrusted inputs]
direction TB
RT[roots allowlist<br/>--root · realpath · no cwd fallback]
CP[compile → manifestDigest]
CK[checks · profile<br/>fail closed]
AP[apply<br/>confirmDigest · TOCTOU · lock]
HK[gate --stdin<br/>PreToolUse, fail closed]
end
subgraph operator [Operator-owned state]
PR[.axiom/profiles/*.json<br/>gate-profile.json]
TS[.axiom/trust/keys.json<br/>state.json + state.key]
FL[CLI-only switches:<br/>--allow-guards · --allow-net · gc]
end
subgraph proof [Proof that survives]
JN[journal · applied marker]
SG[DSSE signatures]
AT[in-toto attestation → Sigstore]
end
AG -->|Plan| CP --> CK --> AP --> JN
AG -->|raw write tool call| HK
AG -.->|cannot reach| FL
RT --> CP & CK & AP
PR --> CK & HK
TS --> CK
AP -->|verify --tree| AT
CP -->|axiom sign, key never in server| SG --> CK
Boundaries, from the outside in
Section titled “Boundaries, from the outside in”1. Roots: an explicit allowlist, no fallback
Section titled “1. Roots: an explicit allowlist, no fallback”The server (axiom mcp --root A --root B) and every CLI verb act only on directories named up
front. Each is realpathed at startup and must be a directory; the set is frozen. A tool’s root
argument is realpathed too and must equal an allowlisted root or lie inside one (case-insensitive
on Windows). There is no cwd fallback, no environment variable, no .git walk-up, and the MCP
client’s roots/list is not consulted (it is untrusted, and deprecated in the 2026-07-28 spec).
What this buys: an agent cannot make AXIOM write outside the repositories an operator chose, no
matter what path it puts in a Plan — .., absolute paths, drive letters, backslashes are rejected
by the RelPath schema before any I/O, and symlinked or junctioned ancestors by the containment
walk (ERR_SYMLINK_IN_PATH, ERR_CONTAINMENT).
The one place cwd is accepted is the PreToolUse hook, where the harness itself spawns the
process in the project directory and puts that directory in the payload — it is the harness’s
declared root, not an accident (hooks.md).
2. The digest a human confirms
Section titled “2. The digest a human confirms”axiom_apply (and axiom apply --confirm) refuses unless confirmDigest === manifestDigest.
The digest is stable across machines (invariant 1) and identifies exactly one set of bytes at
exactly one set of paths (invariant 2), so an approval prompt that shows it — codai’s harness
does, and a destructiveHint: true annotation lets any MCP client ask — is approving something
concrete. Between the dry-run a reviewer looked at and the apply, nothing can be swapped: the
bundle is re-canonicalised and re-hashed, every blob is re-hashed, and every pre-image is
re-hashed at commit (pipeline.md).
3. Policy that fails closed
Section titled “3. Policy that fails closed”Checks are the operator’s rules over the whole change set. Two properties make them a boundary rather than advice:
erroris a verdict, and it blocks. An unknown predicate, invalid params, a provider that could not run (no root forrepo.*, guards not enabled,cedar-wasmabsent, a guard that timed out or printed garbage), a driftedpreImage— all yieldverdict: "error", whichapplytreats exactly likefail. There is no configuration that turns a non-answer into a pass.- The dangerous switches are not tools.
guard.externalruns only when the process was started with--allow-guardsand the profile setsfacts.allowGuards; network fetching ofrefsources isaxiom compile --allow-neton the CLI only;axiom gchas no MCP tool. An agent talking to the server cannot enable any of them.
The PreToolUse gate applies the same rule to raw harness writes (D-18): malformed payload, a
write tool whose target it cannot determine, an unreadable profile — all deny with a reason.
--fail-open exists for editors that must never be blocked by the gate’s own bugs, and is the
operator’s choice, in the hook config.
Profiles live in the repository (.axiom/profiles/<name>.json, .axiom/gate-profile.json) and
are meant to be committed and reviewed like CI configuration (profiles.md).
4. Signed manifests and anti-rollback
Section titled “4. Signed manifests and anti-rollback”When “the agent compiled it” is not enough provenance, a root can pin Ed25519 public keys in
.axiom/trust/keys.json and require, through manifest.requireSigned, that every bundle it
applies carries a detached DSSE envelope over JCS(manifest) from one of those keys. Because the
signature covers the canonical body, any byte change after signing is signature.bad; a key the
root does not know is signature.unknownKey; the trust store missing is error, not pass.
Two extensions close the replay holes:
- Anti-rollback.
Plan.counteris inside the hash and therefore under the signature; withantiRollback: truethe root rememberslastCounterinstate.json(HMAC-authenticated with a per-rootstate.key) and refusescounter ≤ lastCounter, so an old but genuinely signed bundle cannot undo a newer one. The state advances only onstatus: "applied". - Root binding. A store with a
rootIdaccepts only envelopes signed for that id, so a bundle signed for repo A is not a valid signed bundle for repo B.
Private keys never enter the server: signing is axiom sign with AXIOM_SIGNING_KEY or
--key-file, typically in a CI job with a protected secret. Full protocol, key ceremony and the
explicit non-goals (Ed25519 only, no Sigstore keyless, integrity not confidentiality):
signing.md.
5. Attestation: proof that outlives the agent
Section titled “5. Attestation: proof that outlives the agent”axiom verify --tree <root> asks whether a real tree matches a manifest — every artifact present
with its digest, every delete absent — and with --attest writes an in-toto Statement
(predicateType: https://axiom.dev/attestation/apply/v1, subject = the manifest digest plus one
entry per path, no clock). The GitHub Action uploads it through actions/attest, so months later
gh attestation verify can prove which workflow, on which commit, verified that tree against that
digest (verify-tree.md). This is a different proof from a DSSE
signature: signing says who produced the manifest; attestation says a tree was checked against
it, here, then.
What the model does not cover
Section titled “What the model does not cover”Be explicit about the edges, because a trust model that overstates itself is worse than none:
- A writer that ignores
.axiom/lock— an editor, agit checkout, another tool — can still race. The TOCTOU check narrows the window to the instant before each rename and detects the change; it cannot prevent it without a snapshotting filesystem. - The shell scan in the gate is a heuristic. It catches
>,tee,rm,sed -i,git reset --hardand friends in a command string; it does not followcd, aliases, scripts,xargs, or writes done from inside an interpreter. The guarantee isPlan → check → apply; the gate is the seatbelt for everything that bypasses it. - Harness timeouts fail open in every harness AXIOM knows. That is the harness’s decision and
the reason the gate is a ~100 ms lazy chunk with no SDK — and why
npxin a hook is banned (install.md). - Key compromise before revocation is trusted;
trust removeandnotBeforeare the tools. - An attacker who can read and write
.axiom/trust/can deletestate.jsonandstate.keyand restart the counter;minCounteron the store bounds that. Protect the trust directory like CI configuration. - Confidentiality is out of scope: manifests, blobs and journals are not encrypted.
The repository’s own threat model, hardening list and disclosure process are in SECURITY.md.
See also
- Invariants — the rules behind each boundary
- Signing — envelopes, trust store, anti-rollback, root binding
- Verify tree — attestation and how to check it later
- Hooks — the fail-closed gate contract