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-appalter policy simulatealter policy rules createalter policy rules listalter policy rules getalter policy rules updatealter policy rules deleteAll commands are app-scoped — pass --app <id> (or the app’s name), or link a workspace / set ALTER_APP_ID.
Reading app policy
Section titled “Reading app policy”alter policy show-appalter policy show-app --app <app-id-or-name> --output jsonSummarizes 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.
Simulating a request
Section titled “Simulating a request”# "If a request used this grant, what gates it?" — verdict + per-rule tracealter policy simulate --grant <grant-id>
# Method + endpoint, so method/endpoint restriction rules are evaluatedalter policy simulate --grant <grant-id> --method GET --endpoint /repos/octo/hello
# As a specific agent, from a specific IP, at a specific timealter policy simulate --grant <grant-id> --agent <agent-id> \ --client-ip 203.0.113.7 --at 2026-05-21T09:00:00Z
# Content rules: name the operation explicitly and supply a simulated parameter bagalter policy simulate --grant <grant-id> --operation gmail.users.messages.send \ --params '{"recipients": ["bob@other.io"]}'
# Full composed verdict (effect + deny attribution + per-rule trace) as JSONalter policy simulate --grant <grant-id> --endpoint /repos/octo/hello --output jsonDry-runs the app’s custom policy rules for one request shape and prints whether they permit or deny it, plus the per-rule trace. Each trace row identifies a denial, approval requirement, redaction, recent-sign-in requirement, a plain match, or non-match. It is a read with no side effects — nothing is written, no credential is touched — and needs dashboard_app_policy:read (the same permission as the read commands; no separate token permission).
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 simulator 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 JSON-encoded characters. Arrays and scalars are rejected locally; values are used only for the dry-run and never stored. 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.
Policy rules
Section titled “Policy rules”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.
Targeting
Section titled “Targeting”Each rule attaches to exactly one target, picked by a flag (the flags are mutually exclusive):
| Flags | Rule 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.
Permissions
Section titled “Permissions”Token permissions are split by what the operation can do:
| Command | Needs | Notes |
|---|---|---|
rules list / rules get | dashboard_app_policy:read | |
rules create | dashboard_app_policy:rules_create | Creating a rule only ever restricts access — a routine automation permission |
rules update | dashboard_app_policy:rules_update | Disabling a rule loosens enforcement, so this is never granted by a wildcard — select it explicitly when creating the token |
rules delete | dashboard_app_policy:rules_delete | Removing a deny rule loosens enforcement — also never granted by a wildcard |
Create
Section titled “Create”# 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 lateralter policy rules create --agent <agent-id> --body @rule.json --disabled
# Provider-scopedecho '{"when": {"method": "DELETE"}, "effect": "deny"}' | \ alter policy rules create --provider google --body -
# IP allowlist: deny every request NOT from these addresses / rangesecho '{"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 timezoneecho '{"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"
# Require approval: hold matching requests for human approval (does not deny)echo '{"effect": "require_approval", "when": {"method": ["POST", "DELETE"]}, "approval": {"approvers": ["lead@acme.com"], "channels": ["email"]}}' | \ alter policy rules create --type require_approval --body - --name "Approve writes"
# Parameter-conditional approval: only payments over 1,000 need sign-offecho '{"effect": "require_approval", "when": {"match": {"families": ["payment"]}, "params": [{"name": "amount", "op": "gt", "value": 1000}]}, "approval": {"approvers": ["cfo@acme.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 limitecho '{"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 parametersecho '{"match": {"families": ["send"]}, "params": [{"name": "recipients", "op": "not_subset_of", "value": ["*@acme.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):
--type | Body shape (generated from the policy-language spec) | Effect on matching requests |
|---|---|---|
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. |
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), 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 and /repos/octo/hello). 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. List values for any_in/not_subset_of support per-entry glob patterns (*@acme.com); numeric ops take a number, equals a string or number. 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.
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 type | Limits |
|---|---|
content_match | max 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_allowlist | max allow entries: 100 · max entry len: 49 |
json_match | max when list values: 100 · max when value len: 512 |
quota | max limit: 1,000,000 |
require_approval | default 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 |
restriction | max endpoint pattern len: 512 · max endpoints: 50 · max methods: 7 |
time_window | max 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.
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
List / get
Section titled “List / get”alter policy rules listalter policy rules list --agent <agent-id> --output jsonalter policy rules list --limit 100 --offset 0alter policy rules get --rule <rule-id> --grant <grant-id>list accepts --limit (1–100) 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.
Update
Section titled “Update”alter policy rules update --rule <rule-id> --disablealter policy rules update --rule <rule-id> --enablealter policy rules update --agent <agent-id> --rule <rule-id> --name "New name" --description "Why"alter policy rules update --rule <rule-id> --body @new-rule.jsonPartial 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.
Delete
Section titled “Delete”alter policy rules delete --rule <rule-id>alter policy rules delete --provider google --rule <rule-id> --yes # CI / non-interactiveDeleting a deny rule means requests it currently blocks will be allowed, so the CLI asks for confirmation on a terminal; pass --yes in scripts. As with update, a non-app-level rule is only found when its target flag is repeated.
Recipes
Section titled “Recipes”Snapshot the app-level rules for review or diffing in CI
alter policy show-app --output json > app-policy-rules.jsonApply a standard ruleset to an app (policy as code)
for body in rules/*.json; do alter policy rules create --app "$APP_ID" --body "@$body" --name "$(basename "$body" .json)"doneAudit which rules are currently disabled
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.
Related
Section titled “Related”- Policies — the policy model
alter audit— see policy decisions on real calls (alter audit list --policy-decision DENY)