Skip to content

Reference

alter policy

Manage policy rules and read the app-level policy posture from the command line.

Manage policy rules — rules that restrict or gate what requests are allowed (deny a request, rate-limit it, or hold it for human approval) — at the app, agent, grant, or provider level, and summarize an app’s app-level rules at a glance. App-level policy is rules-based: IP and time-of-day restrictions are ordinary policy rules (the ip_allowlist and time_window rule types below), per-agent rate limits are the quota rule type, operation- and parameter-level content rules (deny, redact, or step-up a specific provider operation by its parameters) are the content_match rule type, and “require human approval” is the require_approval rule type, all managed with the same commands as every other rule.

alter policy show-app
alter policy simulate
alter policy validate
alter policy export
alter policy test
alter policy plan
alter policy show-plan
alter policy apply
alter policy rules create
alter policy rules list
alter policy rules get
alter policy rules update
alter policy rules delete

Commands are app-scoped. Interactive rule commands and show-plan accept --app <id> (or the app’s name), a linked workspace, or ALTER_APP_ID. File commands take the app UUID from bindings. apply takes it exclusively from the saved plan and has no --app flag.

The policy-as-code guide contains complete manifest, binding and before/after test examples, ownership rules and protected CI guidance.

Terminal window
alter policy validate --manifest @policy.json --bindings @bindings.json
alter policy export --manifest @export-targets.json --bindings @bindings.json --adopt-existing
alter policy test --manifest @policy.json --bindings @bindings.json --tests @tests.json
alter policy plan --manifest @policy.json --bindings @bindings.json --tests @tests.json --save reviewed-plan.json
alter policy show-plan --app <app-id> --plan-id <plan-id>
alter policy apply --plan @reviewed-plan.json --yes

File operations require dashboard_app_policy:read. plan also requires dashboard_app_policy:rules_create, because it saves a reviewed artifact that a later apply executes. apply additionally checks the saved plan’s current mutation permissions, even on a replay. For validate/export/test/plan, optional --app must match the bindings. Source flags accept @file or - (only one stdin input). --save creates a private new file and refuses overwrite. --adopt-existing is opt-in; export itself never adopts or applies. Apply prompts unless --yes is supplied.

All six file commands accept --output json|jsonl|table (default json). plan emits the exact plan_id field for show-plan --plan-id; the saved file also contains app_id and digest required by apply. If saving fails, the new plan remains on stdout and stderr names its recovery command; the existing file is untouched. test exits 1 for failed fixtures or uncovered changes and 0 for passing evidence. Human review on stderr shows each fixture’s pass/fail and uncovered rule keys; it does not attest live authorization.

For each refusal in the table below, a refused apply names its code on stderr, followed by the remedy, and does not print the server’s own error text. plan names policy_plan_busy the same way. For policy_plan_stale the CLI also prints the server’s reason, for example (reason: preserved_layers_changed). apply compares the saved digest with the server plan itself, so policy_plan_digest_mismatch is reported before any apply is sent. Nothing is saved or applied in any of these cases:

CodeExitMeaning and remedy
policy_plan_stale5Policy state changed after the plan was reviewed. The reason is bound_rules_changed, preserved_layers_changed (a personal rule on a bound target) or ownership_changed. Run plan again and review the new plan.
policy_plan_conflict5Policy state changed while applying. Run plan again and review the new plan.
policy_plan_busy75Another policy change held a lock. Retry the same command, plan or apply, unchanged, with backoff. A retried plan is safe because the refused one saved nothing, and --save writes the file only after a success. A retried apply is safe because a plan that already committed returns its committed result; after a busy apply, do not re-plan or edit the saved file.
policy_plan_digest_mismatch5The plan file was modified, or is not the file plan --save wrote. Apply the unmodified saved file, or run plan again and review the new plan.
policy_plan_expired1The plan expired. Run plan again and review the new plan.
policy_manager_changed5The plan’s policy manager changed while applying. Run plan again and review the new plan.
policy_tests_failed5The plan has no passing fixture results. Fix the manifest or tests, then run plan again.
Terminal window
alter policy show-app
alter policy show-app --app <app-id-or-name> --output json

Summarizes the app-level policy rules — the rules that apply to every request in the app — as a table of id, type, enabled, and name (--output json emits the full rows). It is the read-only posture view of the same rules alter policy rules list manages; needs dashboard_app_policy:read.

Rules that apply across an entire organization are dashboard-only by design — like org-wide key policy (where even reading is dashboard-only), a single scripted call should never see or weaken org-wide security, so there is deliberately no CLI surface for them.

Terminal window
# "If a request used this grant, what gates it?" — verdict + per-rule trace
alter policy simulate --grant <grant-id>
# Method + endpoint, so method/endpoint restriction rules are evaluated
alter policy simulate --grant <grant-id> --method GET --endpoint /repos/octo/hello
# As a specific agent, from a specific IP, at a specific time
alter policy simulate --grant <grant-id> --agent <agent-id> \
--client-ip 203.0.113.7 --at 2026-05-21T09:00:00Z
# Match key-specific conditions with an ID from this app, never the secret
alter policy simulate --grant <grant-id> --api-key-id <key-id> --method POST
# Content rules: name the operation explicitly and supply a simulated parameter bag
alter policy simulate --grant <grant-id> --operation gmail.users.messages.send \
--params '{"recipients": ["bob@example.org"]}'
# Full composed verdict (effect + deny attribution + per-rule trace) as JSON
alter policy simulate --grant <grant-id> --endpoint /repos/octo/hello --output json
# Test an app-wide rule before creating it (full create object, never stored)
cat > candidate-rule.json <<'JSON'
{"rule_type":"json_match","rule_body":{"when":{"method":"POST"},"effect":"deny"},"name":"Block POST"}
JSON
alter policy simulate --grant <grant-id> --method POST \
--candidate @candidate-rule.json

Dry-runs the app’s custom policy rules for one request shape. The headline distinguishes a block, human approval, redaction, recent sign-in, unevaluated runtime review, and incomplete evidence. “No policy block found” is not authorization: ownership, key/agent scopes, grant validity, and runtime conditions can still refuse the call. The per-rule trace retains each independent outcome; approval never overrides a block. The JSON effect remains the deterministic chain’s permit/deny, not a guarantee of execution. An AI rule skipped for a dry run has evidence_status: "runtime_review"; its canonical outcome: "pass" means the dry-run evaluator added no block, not that live AI review passed. No request is executed, quota consumed, approval created, or AI model contacted. It needs dashboard_app_policy:read (the same permission as the read commands).

Rules needing unsupplied API-key identity, client IP, or endpoint have evidence_status: "context_required" and context_gaps identifying the missing inputs. Supply --api-key-id, --client-ip, or --endpoint where prompted. A key ID must belong to the selected app; it supplies policy context only, without checking credential validity, scopes, or authentication. Missing input is not flagged when another known condition already proves the rule cannot apply, or a known method restriction already proves denial. The canonical JSON effect and six-value outcome contract are preserved for existing clients; evidence annotations are additive. Do not treat the supplied-context result as a prediction for missing context.

When run with a PAT (or otherwise outside an authenticated operator session), the simulation excludes organization-level policy definitions from the verdict — org rules are dashboard-only — and the command prints a warning to that effect on stderr. Use the operator dashboard’s Explain a request for a verdict that includes org rules.

It mirrors the dashboard’s policy explorer inputs: --grant (required) is the credential the simulated request uses; --method defaults to GET and accepts 1–16 letters (normalized to uppercase); --endpoint is a 1–512-character provider API path, required to evaluate method/endpoint restriction rules; --agent; --client-ip is a bare IPv4/IPv6 address, not a CIDR, for ip_allowlist rules; --at is an ISO 8601 instant for time_window rules; --operation is a non-empty operation id of at most 255 characters for content_match rules (omit it to classify from --endpoint, exactly like the live gate); and --params is the simulated parameter bag for content_match rules, as a JSON object of up to 20 entries and 8,192 UTF-8 bytes when compact JSON-encoded. Arrays and scalars are rejected locally; values are used only for the dry-run and never stored. --candidate @file.json (or --candidate - for stdin) accepts one complete app-level create object — rule_type, rule_body, and optional name, description, and enabled — validates it locally, overlays it on the existing chain for this request only, and marks it candidate in the trace. Setting enabled to false is accepted but the rule is not overlaid and gets no trace row, because a disabled rule is never enforced once saved either; the command says so on stderr. It never calls the create route. The table form prints the verdict summary and per-rule trace; --output json emits the full composed object.

When a content_match classification ran, the verdict also names the operation the request classified to (or warns that the request is unclassified — content rules targeting the provider deny unclassified traffic), exactly as the dashboard’s explorer shows it; in the JSON form this is the classified_operation / classification_reason pair.

The dry-run covers the custom policy-rule chain, including rules that emit approval/redaction/sign-in obligations, and provider scope-narrowing for delegated or narrowed grants. It does not execute those obligations or apply the grant-validity floor. When the selected grant is terminal or a delegated chain is resolved structure-only, the command prints a “grant liveness not verified” caveat: the grant or an ancestor could deny the live request.

A policy rule is evaluated on every request, on top of a default-deny base: a request already needs ownership of the grant, the right scope, and a valid grant before any rule runs. When a rule’s conditions match, it either denies the request or holds it for human approval (require_approval). Rules can only restrict that base — creating one never grants or widens access.

Each rule attaches to exactly one target, picked by a flag (the flags are mutually exclusive):

FlagsRule applies to
(none)every request in the app
--agent <agent-id>one agent only
--grant <grant-id>one grant (credential) only
--provider <provider>every grant on one OAuth provider (e.g. --provider google)

The target is part of the rule’s address: get, update, and delete must repeat the target flag the rule was created with — a rule created with --agent is only found with the same --agent <agent-id>, and without it the command reports the rule as not found. Provider targeting covers OAuth providers only; for a managed-secret credential, target its grant with --grant instead.

Rules end users set on their own accounts (from their wallet) are not addressable here — only the user can manage those.

Token permissions are split by what the operation can do:

CommandNeedsNotes
rules list / rules getdashboard_app_policy:read
rules createdashboard_app_policy:rules_createCreating a rule only ever restricts access — a routine automation permission
rules updatedashboard_app_policy:read and dashboard_app_policy:rules_updateRead is needed for the before/after review. Disabling a rule loosens enforcement, so the update permission is never granted by a wildcard — select it explicitly when creating the token
rules deletedashboard_app_policy:read and dashboard_app_policy:rules_deleteRead is needed for the before/after review. Removing a deny rule loosens enforcement — delete is also never granted by a wildcard

Every change prints a review to stderr; JSON results stay on stdout. It starts with What this change does: one line per change and, where the rule’s meaning settles it, a rating (More restrictive, Less restrictive, Mixed, or Impact uncertain), plus a warning for any less-restrictive change. The complete Before / After definitions follow. Creating a rule only narrows access, so it proceeds without a prompt; updates and deletes ask for confirmation (see below). Creating a disabled rule does not enforce it.

Terminal window
# App-wide, body from stdin (json_match is the default --type)
echo '{"when": {"method": ["POST", "DELETE"]}, "effect": "deny"}' | \
alter policy rules create --body - --name "Read-only app"
# Agent-scoped, body from a file; create disabled and enable later
alter policy rules create --agent <agent-id> --body @rule.json --disabled
# Provider-scoped
echo '{"when": {"method": "DELETE"}, "effect": "deny"}' | \
alter policy rules create --provider google --body -
# IP allowlist: deny every request NOT from these addresses / ranges
echo '{"allow": ["203.0.113.5", "10.0.0.0/8"]}' | \
alter policy rules create --type ip_allowlist --body - --name "Office egress only"
# Time window: deny requests outside business hours in the given timezone
echo '{"windows": [{"days": ["mon","tue","wed","thu","fri"], "start": "09:00", "end": "17:00"}], "timezone": "America/New_York"}' | \
alter policy rules create --type time_window --body - --name "Business hours"
# Human in the loop (HITL): hold matching requests for human approval (does not deny)
echo '{"effect": "require_approval", "when": {"method": ["POST", "DELETE"]}, "approval": {"approvers": ["lead@example.com"], "channels": ["email"]}}' | \
alter policy rules create --type require_approval --body - --name "Approve writes"
# Parameter-conditional approval: only payments over 1,000 need sign-off
echo '{"effect": "require_approval", "when": {"match": {"families": ["payment"]}, "params": [{"name": "amount", "op": "gt", "value": 1000}]}, "approval": {"approvers": ["cfo@example.com"]}}' | \
alter policy rules create --type require_approval --body - --name "Approve large payments"
# Restriction: method/endpoint allowlist — deny anything NOT on it (read-only repos)
echo '{"allowed_methods": ["GET"], "allowed_endpoints": ["/repos/**"]}' | \
alter policy rules create --type restriction --body - --name "Read-only repos"
# Quota: at most N calls per fixed window (per agent) — deny with a retry-after over the limit
echo '{"limit": 1000, "period": "hour"}' | \
alter policy rules create --type quota --body - --name "1000/hour per agent"
# Content match: deny (or redact / step-up) specific provider operations by their parameters
echo '{"match": {"families": ["send"]}, "params": [{"name": "recipients", "op": "not_subset_of", "value": ["*@example.com"]}], "effect": "deny"}' | \
alter policy rules create --type content_match --body - --name "Internal recipients only"

--body takes @<file> or - for stdin (the same intake convention as every other JSON-accepting alter flag). --type picks the rule’s evaluator and the body shape that goes with it (default json_match):

--typeBody shape (generated from the policy-language spec)Effect on matching requests
ai_review{"effect": "require_approval", "review": {"threshold_level": <int 1-4>, "tail_mass"?: <value>, "confidence_floor"?: <value>, "families"?: ["write"/"send"/"delete"/"admin"/"payment", ...], "triggers"?: [{...}, ...]}, "approval": {...}}Ask an AI model to rate how risky a provider call is and require human approval above a threshold. Never denies, never runs on reads.
content_match{"match": {"operations"?: ["<string>", ...], "families"?: ["admin"/"delete"/"payment"/"read"/"send"/"write", ...]} (at least one), "params"?: [{...}, ...], "effect": "deny"/"redact"/"step_up", "redact"?: {"fields": ["<string>", ...]}, "step_up"?: {"max_session_age_seconds": <int 1-86,400>}}Bind attested catalog operations/families and (optionally) their projected parameters; on a match, deny the request, redact fields from it, or require a fresh (step-up) user session.
ip_allowlist{"allow": ["<string>", ...]}Restrict the request’s source IP to an allowlist of addresses / CIDR ranges (narrowing; requests from any other IP are denied).
json_match (default){"when": {...}, "effect": "deny"}Deny the request when a flat set of request-metadata conditions all match (string equality or list membership, ANDed).
quota{"limit": <int 1-1,000,000>, "period": "minute"/"hour"/"day"/"month"}Admit at most ‘limit’ requests per fixed UTC calendar window per caller principal; beyond the limit the request is denied with 429 + Retry-After.
require_approval{"effect": "require_approval", "when"?: {...}, "approval": {"approvers"?: ["<string>", ...], "expires_in_seconds"?: <int 60-86,400>, "channels"?: ["email", ...], "step_up_max_session_age_seconds"?: <value>, "max_pending"?: <value>, "result_retention_seconds"?: <value>}}Require human approval before the request executes (a 202 + approval flow), always or only when an optional condition matches.
restriction{"allowed_methods"?: ["DELETE"/"GET"/"HEAD"/"OPTIONS"/"PATCH"/"POST"/…, ...], "allowed_endpoints"?: ["<string>", ...]} (at least one)Allowlist the HTTP methods and/or provider endpoint paths the target’s traffic may use; everything outside the allowlist is denied.
time_window{"windows": [{...}, ...], "timezone": "<string>"}Deny the request when it falls outside the UNION of the configured day/time windows, evaluated in the rule’s IANA timezone.

The CLI validates ai_review bodies locally: the threshold is an integer from 1 to 4; probabilities are bounded; eligible families exclude reads; triggers have unique lowercase IDs, bounded questions and labels, and valid probability thresholds. The rule also requires a valid human-approval block. See the AI review setup and example.

For json_match, when is a map of request attribute → expected value or list of values; ALL conditions must match for the rule to fire, and effect is always "deny". The full matchable-attribute list is in the generated Authoring limits block below. Beyond plain metadata, operation and family match the request’s classified catalog operation (by id, or by semantic family — send, read, write, delete, admin, payment) and also fire on requests that cannot be classified (fail closed); client_ip is exact-IP only (CIDR ranges belong in an ip_allowlist rule); a method condition also gates raw-token retrievals, which confer every method. Every body is validated locally before any network call. Optional: --name, --description, --disabled.

For require_approval, the matching request is held for a human to approve rather than denied. The when map is OPTIONAL (omit it to require approval on every request at the target); it accepts the same attribute matcher as json_match plus the content_match vocabulary — a match object (operations and/or families) and a params condition list — so an approval can be operation- and parameter-conditional: {"effect": "require_approval", "when": {"match": {"families": ["payment"]}, "params": [{"name": "amount", "op": "gt", "value": 1000}]}, "approval": {...}} requires sign-off only for payments over 1,000. An operation-scoped condition on a request that cannot be classified is denied (never approved around). The approval object accepts: approvers (up to 10 approver emails — empty/omitted means the connection’s own owner approves; a rule targeting a managed-secret grant must name at least one explicit approver, because a managed secret has no owner to default to — an approver-less save on one is rejected with a structured 400 naming the fix, rather than denying every gated call at runtime), expires_in_seconds (60–86400, default 600), channels (["email"], or [] for a single-gate approval that sends no email and surfaces only the approval link), and the advanced overrides step_up_max_session_age_seconds (-1 disables step-up, 0 forces a fresh sign-in for every decision, up to 3600), max_pending (1–100), and result_retention_seconds (3600–31,536,000 — one hour to one year). Multi-gate approvals require email delivery for every gate so each required approver receives their own link. Authorable at the app / agent / grant / provider level here; user-level approval rules are authored by the end user in the wallet.

For restriction, a request is denied unless its HTTP method is in allowed_methods (when set) AND its provider API path matches allowed_endpoints (when set); at least one of the two must be present. Each endpoint is either an exact path (/user) or a trailing /** prefix wildcard that matches that prefix and anything below it (/repos/** matches /repos, /repos/, and /repos/octo/hello). Exact entries are byte-exact including the trailing slash — /user does not admit /user/ (a distinct request at the provider); list both forms, or use /user/**, when both should pass. Requests spelled as non-canonical aliases — duplicate slashes (/user//), . segments, backslashes, or percent-encoded separators (%2F) — are denied outright under an endpoint allowlist, and patterns spelled that way are rejected at authoring: their provider-side resolution is ambiguous, so no exact rule could be held to what it names. A trailing /** is the ONLY wildcard form — any other * (mid-path like /repos/*/issues, or a single-segment /repos/*) is rejected (a 400 invalid_rule), not treated as a literal or a wildcard. Methods must be one of GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. This is the same method/endpoint allowlist a grant can carry; authoring it as a rule lets you apply it at the app / agent / grant / provider level (e.g. an app-wide “GET only”). Methods are upper-cased; the backend rejects a path containing a .. traversal segment.

For time_window, the request is denied unless its local time (in the required timezone) falls inside one of the windows. Each window is a non-empty set of days (mon…sun, unique) plus a start/end time ("HH:MM", 24-hour). The window is [start, end) — start inclusive, end exclusive; end may be "24:00" for end-of-day. A start later than end is a valid overnight window anchored to the named day (e.g. fri 22:00 → sat 06:00). Up to 20 windows; timezone is required (no implicit default).

For quota, a request is admitted only while fewer than limit requests have been made in the current fixed window (top of the minute/hour, UTC midnight, or the first of the month), counted per caller principal (the agent when the request runs as one, otherwise the connection). Over the limit, the call is denied with a retry-after until the window resets — this is the only rule type that denies with a rate-limit response rather than a plain policy denial. Not usable as a per-request rule.

For content_match, the rule binds specific provider operations rather than raw paths: match names catalog operation ids ("operations", e.g. gmail.users.messages.send) and/or operation families ("families" — one of send, read, write, delete, admin, payment), and at least one of the two must be present. The optional params list adds parameter conditions that must ALL hold for the rule to fire; each condition is exactly {"name", "op", "value"}, where op is one of equals (scalar equality, case-insensitive for strings), any_in (fires when any projected value matches one of the listed patterns), not_subset_of (fires when the projected values are NOT fully covered by the listed patterns — the recipient-allowlist shape), or the numeric comparisons gt/gte/lt/lte. A condition’s name references a parameter from the operation’s catalog entry or any query-string parameter the live request carries (evaluated identically whether the caller embeds it in the URL or passes it through the SDK’s separate query-parameters argument); the numeric operators require a cataloged numeric parameter — an ad-hoc query value projects as a string and a numeric condition on it denies. List values for any_in/not_subset_of support per-entry glob patterns (*@example.com); numeric ops take a number, equals a string or number. A few providers canonicalize a path parameter so that equivalent spellings of the same key match one rule (SAP S/4HANA, where '0000500001' and '500001' are the same sales order). A glob cannot be compared against a canonicalized value, so a pattern on one of those parameters is rejected when the rule is saved — use equals, or any_in with the exact key values. String comparisons are normalized: equals and the any_in/not_subset_of pattern matching compare case-insensitively with surrounding whitespace trimmed (internal whitespace is significant) — deliberate anti-bypass semantics, so a condition cannot be dodged by a case or padding variant; number-typed parameters compare numerically (a string-typed parameter compares as normalized text). On a set-shaped parameter (for example recipients), equals matches only an exact single value — a multi-member set never matches; use any_in to match a member. effect picks what happens on a match: "deny" blocks the request; "redact" lets it through but strips named redactable fields from the outbound request body before forwarding (requires the redact object); "step_up" requires the end user to have re-authenticated within max_session_age_seconds (1–86400, requires the step_up object). The redact/step_up objects are only valid with their matching effect. Content rules fail closed: a request that cannot be classified as a known operation (or whose parameters cannot be read) is denied while a content rule targets it — it is never let through, and never converted into a redaction or step-up for a request that can’t be named. Ambiguous query spellings fail closed: an uncataloged query key repeated with distinct values, or a query key shadowing a differently-sourced cataloged parameter, denies any condition naming that parameter rather than guessing which value the provider would honor — conditions on other parameters are unaffected. A cataloged scalar query parameter repeated with distinct values cannot be projected at all, so every parameter condition on that operation denies.

For ip_allowlist, each entry is a bare IPv4/IPv6 address or a CIDR range, and a request is denied unless its source IP matches one of them. A /0 catch-all entry — any base with a /0 prefix, including zero-padded forms like /00 or /000 — is rejected locally (exit code 2, before any network call): a rule that matches every source IP is an allow-all, not a restriction, so use a concrete address or a narrower prefix. A split-range set whose entries together cover the whole address space (for example 0.0.0.0/1 + 128.0.0.0/1, or ::/1 + 8000::/1) is rejected the same way, even though no single entry is a /0 — as is an entry covering the entire IPv4-mapped-IPv6 block (::ffff:0.0.0.0/96), which would admit every IPv4 source in mapped form on a dual-stack listener. Each entry’s IP/CIDR shape is validated locally too, so a malformed entry fails fast rather than at the backend.

Authoring limits (generated from the policy-language spec — the same source the policy engine enforces):

Rule typeLimits
ai_reviewmax trigger label chars: 60 · max trigger question chars: 500 · max triggers: 8 · risk levels: 5
content_matchmax match entries: 100 · max operation id len: 255 · max param conditions: 20 · max param name len: 120 · max redact field len: 120 · max redact fields: 50 · max value entries: 200 · max value len: 512 · step up max seconds: 86,400 · step up min seconds: 1
ip_allowlistmax allow entries: 100 · max entry len: 49
json_matchmax when list values: 100 · max when value len: 512
quotamax limit: 1,000,000
require_approvaldefault expiry seconds: 600 · max approvers: 10 · max expiry seconds: 86,400 · max pending max: 100 · max pending min: 1 · min expiry seconds: 60 · result retention max seconds: 31,536,000 · result retention min seconds: 3,600 · step up max seconds: 3,600 · step up min seconds: -1
restrictionmax endpoint pattern len: 512 · max endpoints: 50 · max methods: 7
time_windowmax windows: 20

Every serialized rule body is at most 16,384 bytes; --name is at most 120 characters and --description 2,000. Each target holds at most 100 rules authored through this command; a target may additionally carry the rules DERIVED from its settings block (an agent’s approval configuration, a managed-secret grant’s restrictions and approval configuration), which are managed from that surface and do not count against the 100.

json_match matchable attributes: agent_id, api_key_id, app_id, client_ip, environment, family, method, operation, provider_id, resource_kind.

json_match semantics (from the spec’s notes):

  • method matches case-insensitively; a request with NO method (a raw-token retrieval) SATISFIES a method condition — the raw token confers every method
  • an operation/family condition on an UNCLASSIFIED request is SATISFIED (fail closed — an unnameable request is a capability superset of anything the rule could deny)
  • client_ip is an EXACT-IP match (IPv6-canonicalized); CIDR ranges belong to the ip_allowlist rule type and are rejected here
  • an attribute absent on the request (e.g. agent_id for a non-agent caller) never matches — quiet no-fire, except method/operation/family above
Terminal window
alter policy rules list
alter policy rules list --agent <agent-id> --output json
alter policy rules list --limit 102 --offset 0
alter policy rules get --rule <rule-id> --grant <grant-id>

list accepts --limit (1–102 — the 100 authored rules plus the derived settings rules, so one page always holds a complete target) and --offset, and renders a table by default (--output json|jsonl|table). Every format emits the rule rows themselves — json is the array, matching every other list command (the one exception is managed-secrets access, whose JSON keeps its summary envelope with the per-via counts). When more rules remain, the command prints the next --offset to run on stderr, so a | jq pipeline keeps clean data on stdout.

get preserves JSON as its backward-compatible default. Pass --output table for the complete human-readable explanation: type, effect, status, target/source context, operations, every parameter condition, and rule-specific configuration. --output jsonl remains available for streaming pipelines.

Terminal window
alter policy rules update --rule <rule-id> --disable
alter policy rules update --rule <rule-id> --enable
alter policy rules update --agent <agent-id> --rule <rule-id> --name "New name" --description "Why"
alter policy rules update --rule <rule-id> --body @new-rule.json

Partial update — only the flags you pass change (--enable and --disable are mutually exclusive; pass at least one change flag). A rule’s evaluator type is fixed at creation; everything else — body, name, description, enabled — is editable in place, and a replacement --body must keep the shape of the rule’s type (an ip_allowlist rule takes a new {"allow": [...]} body, and so on). Remember to repeat the rule’s target flag (--agent / --grant / --provider) for non-app-level rules.

Editing a disabled rule preserves its disabled state. Both enable and disable require review. In automation, --yes also requires --expected-version from the rule you inspected; the CLI never silently substitutes a newer version. For example, after reviewing version 7:

Terminal window
alter policy rules update --rule <rule-id> --disable --yes \
--expected-version 7 --idempotency-key <unique-change-key>

Capture the exact version field from alter policy rules get --rule <rule-id> --output json (for example, jq -r '.version') along with the definition being reviewed. Do not substitute a fresh version immediately before applying an old review.

A rule owned by a policy file (code_owner on rules list / rules get; the table’s OWNER column and the text view’s “Managed by” line) cannot be changed here: rules update and rules delete stop before the review prompt with exit 5, and the server answers 409 policy_rule_managed to any other client. Change it through alter policy plan / apply, or release it from its manifest first (see the policy-as-code guide).

One exception: a rule whose origin is grant_settings or agent_settings (visible on rules list / rules get) is owned by its settings surface — the managed-secret grant editor’s approval/restrictions block or the agent’s approval settings — which replaces it wholesale on every settings save. Every rules update on such a rule (body, name, description, --enable / --disable) is rejected with a 400 pointing at the owning surface: an edit would be silently overwritten by the next settings save, and a toggle would desynchronize the two views. rules delete works on a grant_settings rule (it is exactly “turn the setting off”, and the grant’s settings read-back reflects it immediately); an agent_settings rule rejects delete too — manage it entirely through the agent’s approval settings.

Terminal window
alter policy rules delete --rule <rule-id>
alter policy rules delete --provider google --rule <rule-id> --yes --expected-version 7

Deleting or disabling an active rule removes that restriction; it does not grant access. All other policy layers and access checks still apply. The CLI asks for confirmation on a terminal; scripts require --yes --expected-version <reviewed-version>. As with update, repeat the target flag for non-app-level rules.

Every create, update, and delete carries a retry key (--idempotency-key, generated when omitted); updates and deletes also carry the reviewed version (--expected-version). A stale version fails with HTTP 412 without changing the rule. Both a locally detected stale revision and an HTTP 412 response exit with code 5 (conflict). Read and review the current rule before making a new change. Missing preconditions fail with 428; reusing a key for different work fails with 422.

If a response is lost, retry the same command, payload, target, reviewed version, and --idempotency-key printed on stderr, including --yes to confirm that exact previously reviewed change even if the first attempt already advanced its version or deleted the rule. Within 24 hours the same currently authorized principal receives the original committed response (a rotated or re-issued token in between does not create a second rule), without another mutation or success audit. This also works after a successful delete has removed the row. Do not reuse the key for a different change. After 24 hours, inspect current state before acting: an expired key no longer guarantees deduplication. Authorization is checked on every retry; the receipt cannot restore revoked access.

Snapshot the app-level rules for review or diffing in CI

Terminal window
alter policy show-app --output json > app-policy-rules.json

Review and apply a managed ruleset

Terminal window
alter policy test --manifest @policy.json --bindings @bindings.json --tests @tests.json
alter policy plan --manifest @policy.json --bindings @bindings.json --tests @tests.json --save reviewed-plan.json
alter policy apply --plan @reviewed-plan.json

Use the complete three-file example. A loop of creates does not reconcile ownership, removals or reviewed drift.

Audit which rules are currently disabled

Terminal window
alter policy rules list --output json | jq '.[] | select(.enabled == false)'

This lists app-level rules only — repeat it with --agent / --grant / --provider (or loop over the targets) to cover each rule family.

  • Policies — the policy model
  • alter audit — see policy decisions on real calls (alter audit list --policy-decision DENY)

Report an issue with this page

Necessary

Required for sign-in, security, authorization, and remembering your choices.

Always active

Analytics

Helps us understand which product and documentation features are useful.

Performance diagnostics

Uses performance tracing and privacy-masked session replay to diagnose problems.

You can change these choices at any time from Cookie settings.