Integrate your agent
Once you've seen Axtary decide actions in fake mode, this is how you put it in front of a real agent so its actual tool calls go through policy. Two ways to route a real agent through Axtary: a runtime hook (gates Claude Code's file tools, Cursor's MCP tool calls, or Codex's shell commands and edits, from inside the agent) and the MCP wrapper (gates any MCP client — Claude Code, Cursor, Codex). All use the same policy file and the same hash-chained ledger.
Not sure which? Jump to Which to use at the end — but the short answer is: the hook for in-agent tool calls (Claude Code, Cursor, or Codex), the MCP wrapper for any MCP client, and the dashboard approval queue when you need guaranteed human approval.
Option 1 — Claude Code PreToolUse hook
Gates Claude Code's Read/Glob/Grep/Write/Edit tools and file-touching
Bash commands by normalizing each into an Axtary content action and asking the
local proxy for a decision.
1. Start the proxy in your project (leave it running):
axtary proxy --config axtary.yml
2. Install the hook config in the project:
axtary hook install claude --owner user:you@example.com --repo your-org/your-repo
The command writes .claude/settings.json with the resolved local Axtary CLI
path and preserves unrelated hooks. Re-running it updates the Axtary hook entry
instead of adding duplicates. The generated hook shape is:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Glob|Grep|Write|Edit|NotebookEdit|Bash",
"hooks": [
{
"type": "command",
"command": "/resolved/node /resolved/axtary/packages/cli/dist/bin.js hook claude-code --proxy http://127.0.0.1:7331 --owner user:you@example.com --repo repo:your-org/your-repo",
"timeout": 10
}
]
}
]
}
}
3. In a Claude Code session in that project, ask it to read .env.production
with either Read or a shell command like cat .env.production — it's denied
with the blocked prefixes named, and Claude self-corrects. Every decision is a
ledger record (.axtary/actions.jsonl).
What each decision does — and where it's enforced
| Axtary decision | Claude Code CLI | Claude Code VS Code extension | Notes |
|---|---|---|---|
allow | tool runs | tool runs | |
deny | blocked ✓ | blocked ✓ | works everywhere — secrets/denied paths can't be read or written |
step_up → ask | prompts for approval ✓ | ⚠️ not prompted yet | the tool currently proceeds in the VS Code extension |
Step-up caveat (important, honest): Axtary returns
step_upcorrectly and records it in the ledger, and the Claude Code CLI honors it (prompts you). But the Claude Code VS Code extension currently ignores a PreToolUseaskdecision and falls back to its own permission rules (anthropics/claude-code#13339) — so the edit can proceed without an approval prompt.denyis unaffected. For guaranteed, payload-bound step-up, use the dashboard approval queue / proxy-executed path (axtary run workflow … --hosted-approval), which binds approval to the exact payload hash rather than relying on the editor's prompt.
Known scope
- File-touching
Bashcommands are governed with conservative shell classification (redirections plus common read/write programs such ascat/sed/awk/grep/rm/tee/touch). Copy/move commands (cp/mv/install/rsync, plusddandtar) govern both the source read and the destination write, so copying a secret to an unprotected path is still blocked by the source read. A command with several file ops is authorized in full and takes the most restrictive decision. Commands with no file operation return no opinion and Claude Code's own permissioning applies. - The hook governs file tools as repo content actions. MCP tool calls are gated by the wrapper below, not the hook.
Option 2 — Cursor beforeMCPExecution hook
Gates the MCP tool calls Cursor's agent makes by normalizing recognized
filesystem tools (read_text_file, read_media_file, write_file,
edit_file, …) into the same Axtary content actions and asking the local proxy
for a decision.
1. Start the proxy in your project (leave it running):
axtary proxy --config axtary.yml
2. Install the hook config in the project:
axtary hook install cursor --owner user:you@example.com --repo your-org/your-repo
The command writes .cursor/hooks.json with the resolved local Axtary CLI path,
sets failClosed, and preserves unrelated hooks. Re-running it updates the
Axtary hook entry instead of adding duplicates. The generated hook shape is:
{
"version": 1,
"hooks": {
"beforeMCPExecution": [
{
"command": "/resolved/node /resolved/axtary/packages/cli/dist/bin.js hook cursor --proxy http://127.0.0.1:7331 --owner user:you@example.com --repo repo:your-org/your-repo",
"timeout": 10,
"failClosed": true
}
]
}
}
3. In a Cursor session in that project, have the agent read .env.production
through an MCP filesystem server — Axtary returns deny, Cursor blocks the call,
and the agent sees the reason in agent_message and self-corrects. Every
decision is a ledger record (.axtary/actions.jsonl).
Decisions and fail-closed behavior
allow→ the MCP tool runs;deny→ Cursor blocks it;step_up→ Cursor asks.- The hook maps to Axtary's
{ permission, user_message, agent_message }output. The human seesuser_message; the agent receivesagent_messageso it can correct course. - Fail-closed: Cursor proceeds on a non-zero/unexpected hook exit, so an
unparseable payload or an unreachable proxy is returned as an explicit
deny(set"failClosed": trueas a second backstop). Unrecognized MCP tools return no opinion, leaving Cursor's own permissioning in place.
Known scope
- Recognized tools are the MCP filesystem family; other MCP servers' tools return no opinion (gate them with the MCP wrapper below).
- Shell execution (
beforeShellExecution) is not yet mapped.
Option 3 — Codex PreToolUse hook
Gates the Codex CLI by normalizing its tool calls and asking the local proxy for
a decision. Codex's reliable hook surface is the shell (Bash) tool, so
Axtary inspects the command for a file it reads or writes (e.g. cat .env) and
maps it to a content action; it also maps apply_patch edits and MCP filesystem
tools (best-effort — Codex fires hooks for those less reliably).
1. Start the proxy in your project (leave it running):
axtary proxy --config axtary.yml
2. Install the hook config in the project:
axtary hook install codex --owner user:you@example.com --repo your-org/your-repo
The command writes .codex/hooks.json with the resolved local Axtary CLI path
and preserves unrelated hooks. Re-running it updates the Axtary hook entry
instead of adding duplicates. The generated hook shape is:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|apply_patch",
"hooks": [
{
"type": "command",
"command": "/resolved/node /resolved/axtary/packages/cli/dist/bin.js hook codex --proxy http://127.0.0.1:7331 --owner user:you@example.com --repo repo:your-org/your-repo",
"timeout": 10
}
]
}
]
}
}
(Codex also accepts inline [hooks] tables in config.toml.)
3. In a Codex session in that project, ask it to cat .env — Axtary returns
deny, Codex blocks the command, and the reason is passed back so the agent
self-corrects. Every decision is a ledger record (.axtary/actions.jsonl).
Decisions and known scope
deny→ Codex blocks the call;step_up→ also blocked (deny) with a message to approve via the dashboard/proxy path. (Live-verified: Codex'sPreToolUsehonors onlydeny— it rejects apermissionDecision:"allow"— so for an allow Axtary emits no decision and lets Codex proceed, and it fails closed on step-up rather than rely on an unverified interactive prompt. For guaranteed, payload-bound step-up use the dashboard approval queue.)- Fail-closed: an unparseable payload or unreachable proxy returns an
explicit
deny. Commands with no file operation (e.g.npm test) return no opinion, leaving Codex's own approval in place. - Shell parsing is conservative (redirections + common read/write programs incl.
cat/sed/awk/grep/rm/tee/touch); it does not fully parse shell, but copy/move commands (cp/mv/install/rsync,dd,tar) govern the source read as well as the destination write, socp secret destis blocked by the source read — not just the destination. Exotic invocations (atararchive streamed to stdout, a reader piped mid-pipeline) remain best-effort.apply_patchedits are governed too (every named file; they fired the hook in Codex v0.142.3); MCP-tool coverage is best-effort per Codex's hook reliability.
Option 4 — MCP wrapper (any MCP client)
axtary mcp serve is a stdio MCP server. Every tools/call is normalized, checked against policy with the tool's definition hash pinned (drift = deny), pass-signed, executed through the wrapped upstream, and ledgered.
Run it (demo mode exposes a deterministic read_fixture tool — no upstream needed):
axtary mcp serve --config axtary.yml
# or wrap a real upstream:
axtary mcp serve --config axtary.yml --wrap 'npx -y @modelcontextprotocol/server-everything'
# or wrap a remote Streamable HTTP upstream:
axtary mcp serve --config axtary.yml --wrap-url https://host/mcp
Prove a real server end to end
Run one explicitly selected live tool through the full governed path and write a dashboard-readable evidence receipt:
axtary mcp conformance \
--wrap-url https://mcp.deepwiki.com/mcp \
--tool read_wiki_structure \
--arguments '{"repoName":"facebook/react"}' \
--accept
The command deliberately pins new/drifted definitions only with --accept, executes through policy and ActionPass v1/DPoP, verifies the correlated hash-chained ledger records, proves a clearly labeled synthetic pin mismatch is quarantined, and independently verifies a signed ledger export. It stores hashes and proof state—not auth headers or response content—in .axtary/mcp-conformance.json. The dashboard shows old receipts as stale; this is point-in-time evidence, not uptime monitoring.
Connect it — pick the path that matches your client
Claude Code CLI (most reliable):
claude mcp add axtary -- node /ABS/PATH/axtary/packages/cli/dist/bin.js mcp serve --config /ABS/PATH/axtary.yml
claude mcp list # confirm "axtary" is registered
Claude Code VS Code extension / Cursor — project-scoped config. Create .mcp.json (Cursor: .cursor/mcp.json) in the project root:
{
"mcpServers": {
"axtary": {
"command": "node",
"args": ["/ABS/PATH/axtary/packages/cli/dist/bin.js", "mcp", "serve", "--config", "/ABS/PATH/axtary.yml"]
}
}
}
MCP onboarding gotcha: a project
.mcp.jsonis only loaded when the editor's workspace is that project folder, and you must approve the server when prompted (or via the client's MCP panel //mcp). Opening a different folder, or adding.mcp.jsonto an already-open session, means the server won't appear. If your client has noclaude/CLI on PATH, use the project-config route and open the folder directly.
Then ask the agent: "Call the axtary read_fixture tool with fixtureKey repo_primer." Confirm it landed:
tail -3 .axtary/actions.jsonl # "tool":"mcp.tool.call", decision "allow"
The tool-poisoning block (deterministic, no client needed)
axtary mcp drift-demo # exit 0 iff the drift was blocked
The tool runs at its reviewed hash, then the definition mutates (a hidden exfiltration instruction) and the identical call is denied with mcp_definition_hash_not_allowed before the upstream runs — a name-based allowlist would have executed the mutated tool.
MCP step-up note
step_up decisions for MCP tools currently deny at the wrapper (there's no interactive approval channel inside MCP). Route step-up workflows through the dashboard approval queue instead.
Which to use
- Claude Code, file edits in an editor: the Claude Code hook (with the step-up caveat above).
- Cursor, in-editor MCP filesystem tool calls: the Cursor hook.
- Codex CLI, shell commands and file edits: the Codex hook.
- Any MCP client, or you want definition-hash provenance: the MCP wrapper.
- Guaranteed payload-bound human approval: the dashboard approval queue (works regardless of editor).