Getting Started

Policy Rules

Manage the local ArmorGemini policy for your workspace from inside Gemini CLI

Policy Rules

Control what tools Gemini can use directly from the CLI. ArmorGemini ships six /armor:* slash commands as Gemini CLI native custom commands, copied by the installer into ~/.gemini/commands/armor/. The staging step is local. The confirm step (/armor:yes) writes the policy to $dataDir/policy.json and enforcement kicks in immediately on the current session, then fire-and-forgets the same policy to the ArmorIQ backend for audit.

Commands

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 and fire-and-forgets the same policy to the ArmorIQ backend for audit.
/armor:noDiscard the currently staged proposal.
/armor:helpShow the /armor command help.

Staging is a local, in-workspace operation. Nothing hits the network until /armor:yes, and even then the backend call is fire-and-forget (used for audit and fleet propagation). The local $dataDir/policy.json is the source of truth for what your BeforeTool hook enforces on this machine.

The full stage-then-confirm flow is: /armor:add ... or /armor:template ... to stage with a YAML preview, then /armor:yes to activate (or /armor:no to discard). Only one pending proposal per workspace, 30-minute TTL, cleared on confirm or discard.

How Rules are Evaluated

Enforcement runs inside the BeforeTool hook. For every tool call, the plugin walks four checks and stops at the first block:

  1. Intent drift (local). Tool must be in the registered plan and the plan must be within TTL.
  2. Local policy (local). If $dataDir/policy.json exists, its statements are walked in order and matched against the normalized Gemini-to-backend tool name. 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.
  4. Fail-closed defaults for auth failures. See Core Concepts for the full list.

The local layer is what runs first, so a local forbid blocks the tool without a network round-trip. The backend layer catches anything the local layer missed and enforces org-wide policy on top.

What /armor:add accepts today

/armor:add <verb> <target> [note] is intentionally small. verb is one of allow, deny, or hold, and maps to the backend's canonical armor.policy.v1 effects (permit for allow, forbid for deny and hold). target is a tool name in the Gemini CLI namespace (e.g. web_fetch, read_file, run_shell_command), which the plugin maps to the backend registry names (e.g. WebFetch, Read, Bash) at proposal time. The optional note is passed through as the statement description.

Argument-condition rules (when args contain "...") and data-class rules (for payment data) are not yet accepted by the /armor:add grammar. If you need those, edit $dataDir/policy.json directly (see the schema below) or push a full armor.policy.v1 profile through the ArmorIQ dashboard for the org-wide policy.

Local policy file shape

/armor:yes writes the confirmed policy to $dataDir/policy.json in armor.policy.v1 shape:

{
  "version": "armor.policy.v1",
  "metadata": {
    "name": "workspace-local",
    "revision": 1
  },
  "defaults": {
    "decision": "allow"
  },
  "combining": "deny_overrides",
  "statements": [
    {
      "id": "s1",
      "effect": "forbid",
      "principal": { "any": true },
      "action":    { "eq": "WebFetch" },
      "resource":  { "any": true },
      "conditions": [],
      "description": "external network not allowed here"
    }
  ]
}

The plugin evaluates statements top-to-bottom against the normalized tool name (Gemini-to-backend map, e.g. web_fetch becomes WebFetch). If no statement matches, defaults.decision decides. deny_overrides means a forbid beats a permit on the same call.

Blocking outcomes, not just tools

This is the most important thing to understand about writing policies. Denying a single tool is necessary but not sufficient because Gemini's planner can often route around a single block by using a different tool to achieve the same outcome.

The routing-around case

> /armor:add deny web_fetch
> /armor:yes

> Fetch https://example.com and dump it
● register_intent_plan({ steps: ["run_shell_command", "read_file"] })
● run_shell_command  curl -sS https://example.com > /tmp/page.html
● read_file /tmp/page.html
● Retrieved via curl (web_fetch is denied by policy).

ArmorGemini did exactly what you told it: blocked web_fetch. But Gemini annotated "web_fetch is denied by policy" and switched to run_shell_command with curl. The outcome (fetching the URL) happened anyway.

This is not a bypass. It is a policy authoring problem. The fix is broader rules that cover the outcome you actually want blocked.

Pattern 1: Block the whole outcome, not one tool

> /armor:add deny web_fetch
> /armor:add deny run_shell_command
> /armor:add deny google_web_search
> /armor:yes

That covers the direct network tools and the shell escape hatch. run_shell_command is broad, so pair this with an allow-list for the specific commands you do want.

Pattern 2: Ship a lockdown template and open holes as needed

> /armor:template lockdown
> /armor:yes

lockdown denies all writes and network by default. Then use /armor:add allow <tool> to open the ones you need. This inverts the trust model, deny-by-default is much easier to reason about than allow-by-default with holes.

Pattern 3: Argument-condition rules (backend only for now)

Argument-condition matching (block run_shell_command when the args contain curl, etc.) is not yet accepted by the /armor:add grammar. To use these today, push an armor.policy.v1 policy through the dashboard for the org, which the plugin fetches via GET /policies/profiles/active and merges as the backend layer.

Templates

/armor:template <name> stages a curated policy for common workspace shapes:

TemplateWhat it stages
lockdownDeny everything. Good for review-only sessions where the model should not run any tools.
strict-read-onlyAllow read_file, list_directory, glob, search_file_content. Deny everything else (writes, shell, network).
balancedAllow reads and controlled writes within the workspace. Deny network egress and destructive shell.

Templates behave like any staged change: /armor:template <name> shows the YAML preview, /armor:yes activates it, /armor:no discards it.

For a sensible default that blocks the common outcome-routing cases from the CLI:

> /armor:template balanced
> /armor:yes

> /armor:add deny web_fetch
> /armor:yes

> /armor:add deny google_web_search
> /armor:yes

Add more as needed for your workflow.

Where policies live

PathWhat
$dataDir/policy.jsonThe active local policy. Source of truth for local BeforeTool enforcement. Written by /armor:yes.
$dataDir/policy-pending.jsonCurrently staged proposal (30-minute TTL). Cleared on /armor:yes or /armor:no.
$dataDir/plans/$sessionId.jsonPer-session intent plan file. Cleared on SessionEnd.
~/.armoriq/credentials.jsonArmorIQ API key. Shared with the armoriq CLI and every other Armor* product.

$dataDir defaults to ~/.gemini/armorgemini. Override with ARMORGEMINI_DATA_DIR.

Backend policy on top

The plugin also fetches the org-wide policy from GET /policies/profiles/active and evaluates it in BeforeTool after the local policy layer. Local policy is authoritative for what your machine does. Backend policy is authoritative for what the fleet does. /armor:yes fire-and-forgets your local policy to the backend for audit, so a workspace owner can promote a well-tested local policy to org-wide from the ArmorIQ dashboard at https://platform.armoriq.ai.

On this page