Skip to content

Quickstart

You need Node ≥ 22.14 (install.md lists the other channels):

Terminal window
npm i -g @codai/axiom-mcp # the global bin — required for hooks
cd /path/to/repo
axiom init # MCP entry + PreToolUse hook + profiles + .gitignore
axiom doctor # every check ok/warn/fail; exit 2 when one fails

Every verb below except hooks also works with npx -y @codai/axiom-mcp <verb> in place of axiom.

flowchart LR
  accTitle: Quickstart path
  accDescr: Install the bin, run setup init, watch a deny, explain it, then undo a session.
  P[plan.json] -->|axiom compile| B[bundle.json<br/>manifestDigest]
  B -->|axiom check| R[CheckReport<br/>pass]
  B -->|"axiom apply &#8209;&#8209;dry-run"| D[unified diff]
  B -->|"axiom apply &#8209;&#8209;confirm digest"| T["tree written<br/>.axiom/journal · applied"]
  T -.->|axiom rollback digest| P0[previous bytes]

1. Wire the repo — axiom init + axiom doctor

Section titled “1. Wire the repo — axiom init + axiom doctor”

axiom init [--root <dir>] [--harness auto|copilot|claude|codex|vscode|cursor|windsurf|gemini|junie|cline|kiro|opencode] [--profile-name <name>] [--force] [--dry-run] [--json] writes everything a harness needs in one idempotent step. --root defaults to the current directory. With --harness auto (the default) it picks one harness per marker directory present — .github → copilot, .claude → claude, .codex → codex, .vscode → vscode, .cursor → cursor, .windsurf/.devin → windsurf, .gemini → gemini, .junie → junie, .clinerules → cline, .kiro → kiro, .opencode → opencode — and falls back to copilot + vscode when none exists. --dry-run prints the unified diff of every file it would write and writes nothing. ACP clients (Zed, …) need no file: run the agent behind axiom acp-proxy (harnesses.md § ACP).

File Written for How
.vscode/mcp.json every harness merged: adds servers.axiom (npx -y @codai/axiom-mcp@2 mcp --root ${workspaceFolder}), keeps every other server
.github/hooks/axiom-gate.json copilot, vscode whole file: preToolUse → command axiom gate --stdin --format auto, bash/powershell the same call with an exit mapping (0/2 pass through, a crash or missing node → 2), timeoutSec: 15 (hooks.md)
.claude/settings.json claude merged: appends one hooks.PreToolUse entry (matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash|PowerShell", axiom gate --stdin --format claude, timeout: 15, onFailure: "block") and the PostToolUse check, keeps the rest
.codex/hooks.json + .codex/config.toml codex merged PreToolUse + PostToolUse (--format codex); [mcp_servers.axiom] appended when absent (harnesses.md § Codex)
.cursor/hooks.json, .devin/ or .windsurf/hooks.json, .gemini/settings.json, .junie/mcp/mcp.json, .clinerules/hooks/PreToolUse(.ps1), .kiro/hooks/axiom-gate.json, .opencode/plugins/axiom-gate.js cursor, windsurf, gemini, junie, cline, kiro, opencode one file per harness — the exact shapes are in harnesses.md
.axiom/profiles/<name>.json always whole file: a repo profile extends: "default" with repo.noOverwriteOf (.git/**, .axiom/**, .github/**, lockfiles, .env*), content.noSecrets, path.deny (node_modules, .next, dist, .turbo)
.axiom/gate-profile.json always whole file: the gate’s deny list, noSecrets: true, pii: false, maxBytes: 262144 (hooks.md § Profile file)
.gitignore always appends the missing lines of .axiom/*, !.axiom/profiles/, !.axiom/gate-profile.json (keeps the file’s line endings)

<name> is --profile-name, else the repo directory name lower-cased with every run of characters outside [a-z0-9._-] turned into -, leading non-alphanumerics and trailing -/. stripped, cut to 64 characters (repo if nothing is left). A name equal to a builtin (default, strict, permissive) gets -repo appended, because a profile extending itself is a cycle; an explicit --profile-name that is invalid or a builtin is ERR_INVALID_PROFILE.

Each file is reported with one action:

Action Meaning
created the file did not exist
merged our entry was added to an existing JSON file (mcp.json, .claude/settings.json)
updated --force replaced our entry / our file, or .gitignore gained missing lines
skipped nothing to do, with a reason: up to date, exists (differs; re-run with --force), already configured, already present, not plain JSON; add the entry by hand (JSONC with comments is never rewritten), top level is not an object

Running init twice changes nothing. Every write goes through the same containment check as apply (a symlink or junction in the path is refused) and is atomic. init also notes when the root has no .git and reminds you that hooks call the global bin.

axiom doctor [--root <dir>] [--json] then reads the result back. Each check is ok, warn or fail with a fix hint; the exit code is 0 unless a check failed (then 2) — warnings never fail it.

Check Fails / warns when
root fail: not a directory
bin warn: no global axiom on PATH, or its version differs from this CLI
hook.copilot · hook.claude · hook fail: hook file is not valid JSON · warn: no axiom gate --stdin entry (an npx entry does not count), or no hook config at all
mcp.vscode warn: .vscode/mcp.json unreadable or without servers.axiom
lock warn: a stale .axiom/lock (the next apply reclaims it)
journal fail: interrupted or corrupt journals (hint: axiom rollback <digest>)
chain fail: the journal hash chain is broken (ERR_JOURNAL_CHAIN, hint: axiom verify --journal)
profile.<name> · profile · gate.profile fail: a profile does not load, or gate-profile.json is invalid (the gate then denies every write) · warn: no repo profile
gitignore warn: .axiom/ is not ignored
gate.latency fail: gate --stdin p95 over 5 runs is above 4000 ms (80 % of the 5 s hook timeout, after which every harness fails open) · warn: a benign Write was denied

2. Or point an MCP client at a repo by hand

Section titled “2. Or point an MCP client at a repo by hand”

This is the entry init merges into .vscode/mcp.json (Claude Desktop, Copilot CLI and Codex use their own file and shape — see harnesses.md):

{
"servers": {
"axiom": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@codai/axiom-mcp@2", "mcp", "--root", "${workspaceFolder}"]
}
}
}

--root is an explicit allowlist and may repeat. There is no cwd or environment fallback: a tool call naming a root outside the list is ERR_ROOT_NOT_ALLOWED; with several roots and none named, ERR_ROOT_REQUIRED.

You can skip the MCP client entirely and use the CLI, which is what the rest of the page does — the MCP tools call the same engines.

Make a scratch directory, git init it (optional, but it makes the gate profile’s .git/** rule meaningful), and save plan.json:

{
"apiVersion": "axiom.dev/v2",
"kind": "Plan",
"name": "hello",
"intent": "Add a greeting module and document it.",
"artifacts": [
{ "path": "src/hello.ts",
"source": { "type": "inline", "content": "export const hi = () => 'hi';\n" } },
{ "path": "README.md", "op": "overwrite",
"source": { "type": "inline", "content": "# hello\n" } }
],
"checks": [{ "id": "no-secrets", "predicate": "content.noSecrets", "params": {} }]
}

A Plan is intent plus a list of artifacts, each with a target path, an op (create default, overwrite, delete) and a source. inline is the simplest; cas, ref, patch and template are in plan-format.md. The same Plan as .axm:

axiom "2"
plan hello {
intent "Add a greeting module and document it."
artifact "src/hello.ts" {
inline <<TS
export const hi = () => 'hi';
TS
}
artifact "README.md" {
op overwrite
inline <<MD
# hello
MD
}
check no-secrets using content.noSecrets {}
}

axiom compile plan.axm accepts it directly (axm-syntax.md), and so does axiom compile plan.yaml (or .yml): the same object as YAML 1.2 core schema — exactly one document, a mapping at the root, no custom tags or << merge keys, no duplicate keys, at most 100 alias expansions; a top-level $schema key (editor hint) is dropped. Any YAML problem is ERR_INVALID_PLAN with details.line/column.

Terminal window
axiom compile plan.json --root . -o bundle.json
# → { "manifestDigest": "sha256:…", "artifacts": 2 }

Compile produced a ManifestBundle: the canonical manifest (paths, ops, sha256 digests, no content), its digest, and the content as side-channel blobs. Because you passed --root, the manifest also carries preImage[] — what compile saw on disk for each path — and a copy was stored under .axiom/manifests/<hex>.json.

Terminal window
axiom check bundle.json --root .
# → CheckReport { "verdict": "pass", "preImage": "verified", "findings": [] } exit 0

check ran the default profile plus your Plan’s own no-secrets over the whole set. Verdict is pass | fail | error; anything that could not be evaluated is error, and apply refuses both fail and error.

Terminal window
axiom apply bundle.json --root . --dry-run
# → ApplyResult { "mode": "dry-run", "status": "applied", "diff": "--- /dev/null\n+++ b/src/hello.ts …" }

Dry-run does phase 1 of the two-phase commit — containment, blob hashing, staging, pre-checks — prints a unified diff and removes the staging tree. Nothing in your working tree changed. The manifestDigest in that output is what you confirm next:

Terminal window
axiom apply bundle.json --root . --confirm sha256:<the digest you just saw>
# → ApplyResult { "mode": "fs", "status": "applied", "files": [ …written… ], "journal": ".axiom/journal/<hex>.json" }

--confirm must equal the bundle’s digest (ERR_CONFIRM_DIGEST_MISMATCH otherwise) — a caller must have seen what it is applying. Right before each file is renamed into place its current bytes are hashed again; if anything changed since staging the transaction rolls back (ERR_PREIMAGE_CHANGED). Run the same command again and you get status: noop.

Terminal window
axiom rollback sha256:<digest> --root .
# → { "status": "rolled-back", "steps": [ … ] }

Rollback reverse-replays that apply’s journal from the backups, touching only the paths it recorded. README.md is back to its previous bytes and src/hello.ts is gone.

.axiom/
manifests/<hex>.json the bundle compile stored (root given)
applied/<hex>.json ApplyResult of the committed apply — presence = idempotency marker
journal/<hex>.json the apply's journal (steps, phase)
journal/chain.jsonl hash-chained log of every finished apply (axiom log · verify --journal)
backup/<hex>/ pre-images of overwritten/deleted files (last 3 manifests)
reports/<hex>.json CheckReports written by the MCP server / CLI
cas/sha256/<aa>/<hex> only with --store cas or fetched ref sources
lock only while an apply or gc holds the root
queue/ · intents/ waiters for the lock and in-flight path claims (axiom status)
profiles/<name>.json the repo profile axiom init wrote (tracked in git)
gate-profile.json the hook's profile (tracked in git)
tmp/ case-sensitivity probe scratch

axiom init already added .axiom/* to .gitignore with exceptions for .axiom/profiles/ and .axiom/gate-profile.json, so the two policy files are committed and the state is not — the default profile refuses any manifest that overwrites .axiom/**. axiom status shows the lock, queue and intents; axiom log lists the chain newest first. Full layout and lifecycle: apply.md, cas.md.


See also