How ArmorClaude Works
Architecture, intent enforcement, drift detection, trust operations, and the hook system
How ArmorClaude Works
ArmorClaude is a Claude Code plugin that intercepts every tool call via lifecycle hooks. It enforces a simple rule: Claude must declare what it intends to do before doing it.
Architecture
User Prompt
|
v
UserPromptSubmit Hook
/armor commands → instant policy response
Other prompts → inject directive: "register your plan first"
|
v
Claude calls register_intent_plan MCP tool
Plan validated + stored (local or signed JWT)
|
v
Claude calls a tool (Read, Write, Bash, etc.)
|
v
PreToolUse Hook (ENFORCEMENT)
1. Is this tool in the registered plan?
2. Do the parameters match?
3. Do policy rules allow it?
4. Is the intent token still valid?
5. Is the MCP server approved?
|
ALLOW or DENY
|
v
PostToolUse Hook
Audit log sent to ArmorIQ (if API key set)Hook Events
ArmorClaude registers 7 lifecycle hooks with Claude Code:
| Hook | When | What ArmorClaude Does |
|---|---|---|
| SessionStart | Session opens | Initialize, show enforcement status |
| UserPromptSubmit | User sends a prompt | Process /armor commands, inject plan directive |
| PreToolUse | Before any tool runs | Enforce: check plan, policy, token, MCP server |
| PostToolUse | After tool succeeds | Send audit log to backend |
| PostToolUseFailure | After tool fails | Send failure audit log |
| Stop | End of a turn | Check token expiry |
| SessionEnd | Session closes | Clean up state |
Daemon Architecture
ArmorClaude uses a daemon process for performance. The hook-router tries the daemon socket first (target: under 5ms p50), falling back to in-process handling if the daemon is unavailable:
Hook event → hook-router.mjs
-> Try daemon socket (fast path, ~5ms)
-> Fall back to in-process (30-350ms)The daemon is a long-lived Node.js process that keeps SDK clients warm, batches audit logs, and avoids cold-start overhead per hook event.
Intent Drift Detection
If Claude tries a tool that was not in its declared plan, ArmorClaude blocks it:
ArmorClaude intent drift: tool not in plan (Bash)This prevents prompt injection from silently steering Claude into unauthorized tool use. Claude sees the denial and either re-registers a new plan that includes the tool, or tells the user it cannot perform that action.
Parameter Enforcement
Plan steps can constrain expected parameters. If Claude's actual tool call parameters do not match the plan's declared inputs, the call is blocked:
ArmorClaude intent mismatch: parameters not allowed for WriteMCP Server Trust
By default, ArmorClaude denies tools from unknown MCP servers (mcpDenyByDefault). When an unknown server's tool is called:
- ArmorClaude blocks the call and surfaces it through Claude Code's native tool approval UI
- The user can approve the one-time call
- For persistent trust, use
/armor mcp approve <server>
Approved and denied servers persist across sessions.
Token Lifecycle
With an API key, ArmorClaude gets a signed JWT from the ArmorIQ backend:
- Claude calls
register_intent_planwith its tool list - ArmorClaude sends the plan to the backend's token endpoint
- Backend returns a signed JWT (default TTL: 1 hour)
- Each tool call is validated against the plan embedded in the token
- When the token nears expiry, ArmorClaude auto-refreshes at turn boundaries
Without an API key, the plan is stored locally with no expiry.
Trust Operations
Trust operations modify the active intent token without starting from scratch. These are available as MCP tools:
Revoke
Immediately invalidates the active token. Within ~55ms, every enforcement point in the fleet refuses it. Use when the plan has gone wrong.
Re-anchor
Updates the plan without revoking the token. Signs a delta linking the old plan hash to the new one, preserving the tamper-evident lineage in the audit log.
Delegate
Issues a subtree-bounded child token with a Merkle inclusion proof. A sub-agent receives authority confined to a specific part of the plan (e.g., /steps/[1]).
Crypto Policy Binding (CSRG)
When an API key is configured, ArmorClaude cryptographically binds policies using Merkle trees:
- Policy statements are hashed into a Merkle tree
- The policy hash is included in the signed intent token
- At enforcement time, the hash is verified to ensure the policy hasn't been tampered with
- After any policy change, the crypto binding is automatically reissued
This ensures that the policy evaluated at token-minting time matches what's enforced at tool-call time.
Enforcement Engines
ArmorClaude supports two enforcement engines:
| Engine | Description | When to use |
|---|---|---|
| local (default) | Policy evaluated in-process by the plugin | Single user, low latency |
| opa | Policy delegated to an OPA PDP server | Fleet-wide enforcement, central policy management |
Switch engines with:
/armor settings enforcement opaOPA mode requires ARMORCLAUDE_OPA_PDP_URL to be set and pushes compiled policy bundles to the backend on every policy change.
Fail-Closed
In enforce mode (the default), any of these causes a tool call to be blocked:
- No intent plan registered (and intent is required)
- Tool not in plan (intent drift)
- Parameters do not match plan constraints
- Policy rule denies the tool
- Intent token expired
- MCP server not approved
- Internal hook error
When an API key is configured, every block is recorded in the ArmorIQ audit log with the matched rule, the registered plan, the tool call, and the deny reason - so the chain of evidence is complete even when enforcement is silent to the user. In local-only mode (no API key), blocks still surface in Claude's response but are not shipped to a backend.
Whitelisted Tools
These tools are never blocked (they are needed for ArmorClaude to function):
register_intent_plan- Claude needs this to declare planspolicy_read- Policy reading (read-only)trust_revoke/trust_reanchor/trust_delegate- Trust operationsExitPlanMode- Plan mode approvalToolSearch/TodoWrite/ListMcpResourcesTool- Claude Code internals with no side effects
State Management
The daemon maintains in-memory state, with files as the durable backing store:
| File | What | Lifecycle |
|---|---|---|
runtime.json | Sessions, tokens, plans, discovered tools, trust ops | Per-session, pruned after 24h |
policy.json | Policy rules + version history | Persistent |
pending-plan.*.json | Plan awaiting consumption by PreToolUse | Consumed + deleted on next tool call |
policy-pending.json | Staged policy proposal (awaiting /armor yes) | Expires after 30 minutes |
policy-drafts.json | Draft policies in progress | Until staged or discarded |
Files live in ~/.claude/plugins/data/armorclaude-armoriq/.
No Separate LLM Call
ArmorClaude does not call a separate LLM to generate plans. Claude itself generates the plan as part of its normal reasoning turn, using the session's own credentials. Zero extra cost, zero extra latency.