Core Concepts

How ArmorGemini Works

Architecture, intent enforcement, drift detection, MCP server, and the Gemini CLI hook system

How ArmorGemini Works

ArmorGemini is a Gemini CLI plugin. It wires six lifecycle hooks and bundles a stdio MCP server (armorgemini-policy) that Gemini CLI launches automatically. The rule it enforces is simple: Gemini must declare what it intends to do before doing it, and every tool call is checked against that declared plan, a local policy, and an org-wide backend policy before it runs.

Architecture

User Prompt
     |
     v
SessionStart Hook  (prints ENFORCING banner, resolves API key)
     |
     v
BeforeAgent Hook  (every turn, injects: "register your plan first")
     |
     v
Gemini calls register_intent_plan  (armorgemini-policy MCP tool)
     |
     v
BeforeToolSelection Hook  (no-op today, see Hook Events)
     |
     v
Gemini calls a tool (read_file, run_shell_command, web_fetch, ...)
     |
     v
BeforeTool Hook  (LAYERED ENFORCEMENT)
  1. is the tool in the registered plan?      (drift check, local, no network)
  2. is the plan still fresh?                  (TTL check, no network)
  3. local policy match?                       ($dataDir/policy.json, no network)
  4. POST /iap/enforce                         (backend policy check)
     |
  decision: allow or decision: deny
     |
     v
AfterTool Hook  (POST /iap/audit, best-effort)
     |
     v
SessionEnd Hook  (clears the session's plan file)

Hook Events

ArmorGemini wires six Gemini CLI lifecycle hooks:

HookWhenWhat ArmorGemini Does
SessionStartSession opensLog session_id, cwd, and configured state. Print the ENFORCING banner (with intent required or policy only mode).
BeforeAgentEvery turnInject a directive telling the model to call register_intent_plan (via the bundled armorgemini-policy MCP server) before any other tool.
BeforeToolSelectionBefore each tool selectionNo-op today. Gemini API rejects allowedFunctionNames with mode: "AUTO", and mode: "ANY" forces a tool call on every turn. Enforcement stays in BeforeTool.
BeforeToolBefore any tool runsLayered enforcement: intent-drift check, local policy check, then POST /iap/enforce for org-wide policy. Any layer denying blocks the call.
AfterToolAfter tool succeeds or failsSanitize input (redact obvious secret-shaped keys, truncate long strings) and best-effort POST /iap/audit. Never blocks.
SessionEndSession closesClear the session's plan file.

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.

The Bundled MCP Server

ArmorGemini ships a small stdio MCP server at scripts/policy-mcp.mjs, declared in gemini-extension.json under mcpServers.armorgemini-policy. Gemini CLI launches it automatically on session start and exposes three tools:

ToolPurpose
register_intent_planDeclare your plan for the current turn. Must be called before any other tool when ARMORGEMINI_INTENT_REQUIRED=true (the default).
reset_intent_planClear the current plan explicitly. The next tool call will be denied until a fresh plan is registered.
get_intent_planRead the currently registered plan for a session. Informational.

The plan shape:

{
  "goal": "One-line summary of the task",
  "steps": [
    { "action": "read_file", "description": "Peek at the top of README" },
    { "action": "list_directory", "description": "See what else is in the dir" }
  ]
}

Tools listed in steps[].action are allowed for the rest of the turn. Anything else is denied at BeforeTool as intent drift.

Plans are stored per-session at $dataDir/plans/$sessionId.json and cleared at SessionEnd.

Intent Drift Detection

If Gemini tries a tool that was not in its declared plan, ArmorGemini blocks it in BeforeTool before the tool runs:

X   ArmorGemini intent drift: tool not in plan (web_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.

Drift detection is entirely local. There is no backend hop for the drift check itself, so obvious drift is denied in single-digit milliseconds. The backend policy layer runs after the drift check.

Layered Enforcement in BeforeTool

The BeforeTool hook is where enforcement happens. It runs four checks in order and stops at the first block:

  1. Intent drift (local). Tool must appear in the registered plan and the plan must be within TTL (ARMORGEMINI_PLAN_TTL_SECONDS, default 600s).
  2. Local policy (local). If $dataDir/policy.json exists (written by /armor:yes), the plugin evaluates the tool call against it. permit allows, forbid blocks, with deny_overrides as the conflict resolution.
  3. Backend policy (network). The tool call is sent to POST /iap/enforce, which runs the org's active armor.policy.v1 policy profile server-side and returns an allow or deny verdict.
  4. Fail-closed defaults. If no API key is configured, or the backend returns 401 or 403, the hook denies with a clear "not configured" message. Backend 4xx that are not auth failures (400, network error) fall through to monitor-mode allow so a broken backend does not brick your CLI.

Local policy is the source of truth for what happens on your machine. The backend is the source of truth for the org-wide policy that governs the fleet. /armor:yes writes both.

Slash Commands

Six /armor:* slash commands ship as TOMLs under ~/.gemini/commands/armor/:

CommandPurpose
/armor:listShow the current active local policy.
/armor:add <verb> <target> [note]Stage a rule change (verb: allow, deny, or hold). Shows a YAML preview, does not apply.
/armor:template <name>Stage a named policy template (lockdown, strict-read-only, balanced). Shows a YAML preview, does not apply.
/armor:yesConfirm the currently staged proposal. Writes $dataDir/policy.json (enforcement live immediately) and fire-and-forgets the same policy to the backend for audit.
/armor:noDiscard the currently staged proposal.
/armor:helpShow help.

/armor:yes is the only confirmation. No dashboard round-trip. See Policy Rules for the full stage-then-confirm flow.

Fail-Closed

In enforce mode (the default), any of the following causes a tool call to be blocked:

  • Plugin is not configured with an ArmorIQ API key
  • No intent plan registered and ARMORGEMINI_INTENT_REQUIRED=true
  • Tool not in the registered plan (intent drift)
  • Plan expired (past ARMORGEMINI_PLAN_TTL_SECONDS)
  • Local $dataDir/policy.json returns forbid for the tool
  • Backend POST /iap/enforce returns decision: "deny"
  • Backend returns 401 or 403 (auth failure)

Every block is logged in the plugin's local trace (when ARMORGEMINI_DEBUG=true) and forwarded to the backend audit log by the AfterTool hook.

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, using the bundled MCP server's register_intent_plan tool. Zero extra cost, zero extra latency for planning.

Provenance

ArmorGemini ports the ArmorClaude enforcement model to Gemini CLI. Gemini CLI's hook set is a superset of what Claude Code exposes: BeforeAgent is the per-turn hook (equivalent to Claude's UserPromptSubmit), and BeforeToolSelection is a bonus tightening layer that Claude Code does not have. The plugin bundles a stdio MCP server declared via mcpServers in the gemini-extension.json manifest, so intent-plan capture works natively without shell-side hacks.

On this page