How ArmorGemini Works
Architecture, intent enforcement, drift detection, and the Gemini CLI hook system
How ArmorGemini Works
ArmorGemini is a Gemini CLI hook plugin that intercepts every tool call via the CLI's lifecycle hooks. It enforces a simple rule: Gemini must declare what it intends to do before doing it.
Architecture
User Prompt
|
v
SessionStart Hook (first time in a session)
Inject directive: "register your plan first"
|
v
Gemini emits a register_intent_plan step
Plan captured (local for the session, signed by backend if API key set)
|
v
Gemini calls a tool (read_file, write_file, run_shell, fetch, ...)
|
v
BeforeTool Hook (ENFORCEMENT)
1. Is this tool in the registered plan?
2. Do the parameters match?
3. POST /iap/verify-step: backend verifies intent + policy + CSRG
|
decision: "allow" or decision: "deny"
|
v
AfterTool Hook
POST /iap/audit: send result to ArmorIQ (if API key set)
|
v
SessionEnd Hook
Flush pending audit records, prune old stateHook Events
ArmorGemini wires 4 Gemini CLI lifecycle hooks:
| Hook | When | What ArmorGemini Does |
|---|---|---|
| SessionStart | Session opens | Initialize, resolve API key, show enforcement status, inject plan directive |
| BeforeTool | Before any tool runs | Enforce: verify with backend, check plan, deny drift |
| AfterTool | After tool succeeds or fails | Send audit log to backend |
| SessionEnd | Session closes | Flush pending audits, prune state |
Each hook is a type: "command" entry in ~/.gemini/settings.json that shells out to scripts/hook-router.mjs in the ArmorGemini checkout. The router reads the Gemini CLI's stdin JSON payload, dispatches to the engine, and writes the decision JSON back to stdout. That is the entire integration surface, no long-running daemon, no proxy.
Intent Drift Detection
If Gemini tries a tool that was not in its declared plan, ArmorGemini blocks it:
X ArmorGemini intent drift: tool not in plan (fetch)This prevents prompt injection from silently steering Gemini into unauthorized tool use. Gemini sees the denial and either re-registers a plan that includes the tool, or tells the user it cannot perform that action.
The drift check has two layers:
- Local fast-path: a plain
if (toolName not in plan)check in the hook, so obvious drift is denied in single-digit milliseconds without hitting the network. - Backend verification: the same call is also sent to
POST /iap/verify-step, which runs the full ArmorIQ policy engine (CSRG binding, Merkle-signed policy, cross-session token validity). The backend's decision is authoritative if the two disagree.
The Backend Does the Heavy Lifting
ArmorGemini itself is intentionally thin. It captures the payload, forwards it, and honors the decision. All of the interesting security work happens on the ArmorIQ backend:
| Concern | Endpoint | Where it runs |
|---|---|---|
| Intent verification | POST /iap/verify-step | ArmorIQ backend |
| Policy compilation | POST /policies/profiles/propose, PUT /policies/profiles/draft, POST /policies/profiles/template | ArmorIQ backend |
| CSRG Merkle signing | POST /verify/action | ArmorIQ backend |
| Audit log | POST /iap/audit | ArmorIQ backend |
| Active policy fetch | GET /policies/current | ArmorIQ backend |
This is the same pattern as ArmorClaude and ArmorCodex, the plugin's job is to wire the hook, ship the request, and respect the answer.
Slash Commands
Four /armor:* slash commands ship as TOMLs under ~/.gemini/commands/armor/:
| Command | Purpose |
|---|---|
/armor:list | Show the current ArmorIQ policy for this workspace |
/armor:add <rule> | Stage a policy rule change (verb + target + optional note) |
/armor:template <name> | Stage a named ArmorIQ policy template (lockdown, strict-read-only, balanced, ...) |
/armor:help | Show the /armor command help |
The TOMLs are the Gemini CLI's native custom-command format, they shell to a small CLI script (scripts/armor/cli.mjs) that talks to the ArmorIQ backend. Staged changes go through the policy proposal flow, they are not applied instantly, the user confirms from the ArmorIQ dashboard.
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
- Backend
POST /iap/verify-stepreturnsdecision: "deny" - Intent token expired
- Backend unreachable and the local layer cannot verify
Every block is recorded in the ArmorIQ audit log with the matched rule, the registered plan, the tool call, and the deny reason.
No Separate LLM Call
ArmorGemini does not call a separate LLM to generate plans. Gemini itself generates the plan as part of its normal reasoning turn. Zero extra cost, zero extra latency for planning.