Reference
alter managed-secrets
Manage backend credentials, their grants, and effective access.
Managed secrets are backend-only credentials (API keys, signing secrets) that Alter stores and injects into outbound requests — no OAuth user involved. These commands cover the full lifecycle: the secrets themselves, the grants that hand them to principals, and the helpers that answer “who can reach this secret.”
alter managed-secrets templatesalter managed-secrets listalter managed-secrets show <secret-id>alter managed-secrets create (--input <path> | --name <name> (--credential-value <value> | --credentials <@file.json>)) [...]alter managed-secrets rotate <secret-id> (--input <path> | --credential-value <value> | --credentials <@file.json>) [--template <template-id>] [--skip-preflight] [--confirm-template-migration <slug>]alter managed-secrets delete <secret-id> [--confirm <slug>]alter managed-secrets access <secret-id>alter managed-secrets set-delegation-policy <secret-id> [--allow-group-delegation | --no-allow-group-delegation] [--max-delegation-ttl-days <days>] [--delegable | --no-delegable] [--max-delegation-depth <n>]alter managed-secrets set-allowed-hosts <secret-id> --host <host> [--host ...] | --clearalter managed-secrets usersalter managed-secrets groups list | show <group-id>alter managed-secrets grants list | list-for-agent | get | create | update | revokeAll commands are app-scoped — pass --app <id> (or the app’s name), or link a workspace / set ALTER_APP_ID. Reads need dashboard_secrets:read; create / rotate / set-delegation-policy / set-allowed-hosts / grant create + update need dashboard_secrets:write; grants revoke needs dashboard_secrets:admin (the revoke tier, matching grants revoke); delete needs dashboard_secrets:delete.
templates
Section titled “templates”List the managed-secret template catalog for the app (openai, aws, …). Templates pre-fill the credential type and injection rules.
alter managed-secrets templates--limit accepts 1–1000 (default 200); --offset defaults to 0.
The table’s CREDENTIAL FIELDS column is the authoritative mapping from a
template’s form_schema to CLI flags. A schema field named project_url, for
example, is entered unchanged as
--credential-field project_url=<PROJECT_URL>. Required fields have no suffix;
optional fields are marked (optional). The BINDING column names the form
field a bound template derives its allowed hosts from (Supabase Project APIs
shows project_url); — means the template declares no binding field. Most
— templates have an operator-editable allowlist — Supabase Management is the
exception: its allowlist is permanently bound to the fixed Supabase management
host with no form field, so it shows — yet is immutable, as described under
set-allowed-hosts below. --output json and --output jsonl retain the
complete form_schema and the binding field for scripts.
list / show
Section titled “list / show”alter managed-secrets listalter managed-secrets show <secret-id> # never exposes the credential valuelist accepts --limit 1–1000 (default 100), --offset (default 0) and
--search <substring>, a case-insensitive match on the secret’s name or slug
(up to 255 characters), so one secret can be found by name in an app that
holds more secrets than one page.
Both print UPDATED alongside CREATED. UPDATED is the last write to the
secret — a credential rotation, or a metadata edit such as the delegation
policy or the allowed-host list — so it answers “when was this credential last
changed?” without reading the audit log. rotate returns the same field on its
response, which is how a script confirms the rotation it just issued landed.
create
Section titled “create”Create a managed secret and its base grant. --name is required unless you supply a full --input body.
The slug is server-generated from --name — the name is slugified (e.g. Stripe Production → stripe-production), with a -<N> suffix appended only if that slug is already taken in the app. Name the secret after the provider so the slug is meaningful; the slug is the stable machine handle the SDK, CLI, dashboard, and audit logs reference. A stale request body that still ships a slug field is silently ignored.
| Flag | Description |
|---|---|
--name <name> | Human display name. Required in per-field mode; omitted when --input supplies the body. |
--description <text> | Free-form description. |
--template <id> | Template ID (e.g. openai); omit for a custom secret. |
--credential-type <type> | bearer_token (default), header_key, basic_auth, or aws_sig_v4. |
--credential-value <value> | Primary credential. Required in per-field mode unless --credentials contains credential_value. See credential intake. |
--credentials <@file.json> | Multi-field credentials as a JSON object — preferred for multi-field types. |
--credential-field <k=v> | Repeatable; sets the matching form_schema.fields[].name exactly as printed by managed-secrets templates. |
--injection-header <name> | Header to inject into (default Authorization). |
--injection-format <template> | Header value template (default Bearer {token}). |
--injection-rule <@file.json> | Path-prefixed JSON array of up to 10 additional injection rules; see the shape below. |
--label <label> | Base-grant display label. |
--account-identifier <id> | Optional account identifier metadata stored on the secret. |
--account-display-name <name> | Optional account display name metadata. |
--allow-group-delegation | Allow members of a group-typed grant to delegate this secret to an agent (default: off). |
--max-delegation-ttl-days <days> | Cap on a single delegation’s lifetime, 1–1825 days (default 90). |
--delegable | Developer ceiling: allow grants under this secret to be marked re-delegable, gating onward agent → agent delegation for the whole secret (default: off). |
--max-delegation-depth <n> | Max delegation-chain depth under this secret, 0–8 (global cap is 8). |
--skip-preflight | Save without the provider credential check (audited; use only when the provider endpoint is unavailable). The response’s preflight summary reports skipped so the unverified state is never silent. |
--input <path> | JSON body from a file (@path) or stdin (-); replaces the per-field flags. |
# Single-field bearer token, value piped from stdinprintf '%s' "$OPENAI_KEY" | alter managed-secrets create \ --name "OpenAI" --template openai --credential-value -Custom secrets
Section titled “Custom secrets”Omitting --template creates a custom secret for an API outside the provider catalog — the operator supplies the injection contract the catalog would otherwise provide (see Connect a custom API):
# Custom header-key API, value piped from stdinprintf '%s' "$BILLING_API_KEY" | alter managed-secrets create \ --name "Internal Billing API" \ --credential-type header_key \ --injection-header X-Api-Key \ --injection-format '{token}' \ --credential-value -
# Custom secrets derive no allowed hosts — set the egress allowlist explicitly,# or proxied calls for the secret are blocked (see set-allowed-hosts below)alter managed-secrets set-allowed-hosts <secret-id> --host billing.internal.example.comThe credential preflight always reports skipped for a custom secret — there is no known provider endpoint to verify against, so the credential is verified on first use.
Supabase Project and Management contracts
Section titled “Supabase Project and Management contracts”Supabase exposes separate project and account-management contracts. Use a project key (sb_publishable_, sb_secret_, or a legacy project JWT) with the Project APIs template and supply its exact Project URL:
alter managed-secrets create \ --name "Supabase Project APIs" \ --template supabase \ --credential-value @./supabase-project-key \ --credential-field project_url=https://PROJECT_REF.supabase.coUse an sbp_ personal access token with the Management API template:
alter managed-secrets create \ --name "Supabase Management" \ --template supabase-management \ --credential-value @./supabase-management-patThe CLI and backend reject unknown or cross-product credential shapes. Both templates own their injection contract, so --injection-rule, additional_injections, custom injection headers, and custom injection formats cannot override it. Project credentials are bound to exactly PROJECT_REF.supabase.co and Supabase’s documented /rest/v1, /auth/v1, /storage/v1, /functions/v1, /realtime/v1, and /graphql/v1 surfaces; Management credentials are bound to exactly api.supabase.com. set-allowed-hosts cannot widen or clear either binding.
Rotating a historical supabase row with an sbp_ PAT can atomically reclassify it as supabase-management, remove the project binding, and repoint every existing grant. This one-way action requires --confirm-template-migration <slug>, where <slug> exactly matches the target secret. An --input body may instead carry the same slug in the backend wire field "confirm_template_migration"; if both forms are supplied, they must match. Current v2 Project API secrets cannot be converted in place; create a separate Management secret. The CLI normally reads the current template to mirror dashboard validation. Passing --template skips that lookup for ordinary rotations; a Data-template PAT migration still reads the target so it can verify the slug and eligibility before the mutating request. The server independently rechecks the exact slug, template, contract version, and credential class before storing anything and again under the mutation lock.
Credential preflight
Section titled “Credential preflight”Where the template supports it, the credential is verified against the provider at save time, and the CLI prints the verdict on stderr — exactly what the dashboard shows inline:
passed— the provider accepted the credential.inconclusive— verification couldn’t complete (for example a transient network issue). The save proceeds; verify the key with a test call.skipped— the credential type can’t be probed, or the secret is custom (templateless) and has no provider endpoint to probe. The save proceeds unverified; the message names why.
A credential the provider definitively rejects fails the save with a structured error and nothing is stored. The same verdicts print on rotate.
Credential intake
Section titled “Credential intake”The primary credential value (--credential-value) accepts three forms, in decreasing order of safety:
| Form | Reads from |
|---|---|
--credential-value - | All stdin through EOF (preferred for CI). Surrounding whitespace is trimmed; embedded CR, LF, or null characters are rejected. |
--credential-value @/path/to/file | A file. |
--credential-value <value> | Inline — prints a shell-history warning. |
Multi-field credential types (such as aws_sig_v4, which needs an access key, secret key, and region) take either --credentials @file.json (a JSON object of string fields — it may also include a top-level credential_value to satisfy the primary value in one file) or repeated --credential-field key=value flags. If both are supplied, the file wins and the flags are ignored with a warning.
Run alter managed-secrets templates before create or rotation. Each
form_schema.fields[].name maps unchanged to the flag key. Required fields are
validated by the CLI and backend; provided optional fields must satisfy their
template pattern. For Twilio, for example, the Account SID is the primary value
and auth_token is the required secondary field:
alter managed-secrets create \ --name "Twilio" \ --template twilio \ --credential-value @./twilio-account-sid \ --credential-field 'auth_token=<AUTH_TOKEN>'The placeholder command shows the exact key mapping only. For shell-history
safety, put the real values in one protected JSON file and pass
--credentials @file.json; the object can contain credential_value plus
every printed secondary field.
The file form also reads the complete file and trims surrounding whitespace; embedded CR, LF, or null characters are rejected in every primary-credential input form.
Each --injection-rule entry is
{"target":"header|query_param","key":"<name>","value_source":"token|additional_credentials.<snake_case_field>"}.
key must start with a letter, contain only letters, digits, -, or _, and
be 1–100 characters. The file must begin with @, contain a JSON array, and
hold at most 10 entries.
Provider templates may make injection rules immutable. Supabase Project APIs and Management are header-only provider contracts and reject additional injection rules, including query-parameter injection.
rotate
Section titled “rotate”Swap the stored credential while active grants continue to use the same managed secret. Same credential-intake forms and precedence as create: provide --credential-value, a credential_value in --credentials, or an --input body containing credential_value.
The CLI reads the template catalog to apply the same required-field and pattern
checks as the dashboard. It also reads the target secret to discover its current
template unless you pass --template <template-id> (or
expected_template_id in --input) as an explicit optimistic precondition.
dashboard_secrets:write already implies read access. The explicit precondition
usually avoids the target-secret discovery lookup (a Supabase Data-template PAT
migration still reads the target to verify the slug and eligibility), but the
catalog lookup remains so the template’s credential contract cannot drift
between CLI and dashboard. A secret bound to a template the catalog no longer
lists (a deprecated provider) still rotates: the CLI notes that local field
validation was skipped and the server validates against the secret’s own
stored template.
With --input, --template is an allowed sideband precondition only when the
JSON body does not contain expected_template_id; supplying both is rejected
instead of silently weakening either value. The server verifies the
precondition atomically; a concurrent provider change returns conflict instead
of applying validation for the wrong provider.
For multi-field templates, rotation distinguishes the two field kinds shown by
managed-secrets templates: secret (password) fields are the rotating
credential and must be passed again — Twilio’s auth_token, for example —
while stored text fields (subdomains, hosts, project URLs) are inherited from
the current secret when omitted. Omitting Supabase’s project_url keeps its
current exact project host; omitting Shopify’s store_subdomain keeps the
stored store; omitting a bound template’s binding field (the deployment
endpoint or application ID named on the template) keeps the stored value and
its derived allowed hosts. Provide a text field explicitly only to change it —
for a bound template, rotating with a new binding value is also how the
derived allowlist is retargeted.
Pass --skip-preflight only when the provider verification endpoint is unavailable. It is an audited query-side escape hatch in both per-field and --input modes; the credential body is unchanged, and the response reports the preflight as skipped so the unverified rotation is explicit.
--confirm-template-migration <slug> is only for the historical Supabase Project APIs → Management API repair described above. The exact slug binds confirmation to one target and prevents a reused rotation command from reclassifying a different secret. It does not weaken the server’s template, contract-version, or credential-class checks.
printf '%s' "$NEW_KEY" | alter managed-secrets rotate <secret-id> --credential-value -delete
Section titled “delete”Like apps delete, it’s gated behind the separate dashboard_secrets:delete scope and a type-to-confirm check against the secret’s slug:
- Interactive: the CLI prompts for the slug. This path also needs
dashboard_secrets:read— it fetches the secret to show the slug in the prompt. - Non-interactive (CI): pass
--confirm <slug>. Adashboard_secrets:delete-only PAT (no:read) must use this form.
The explicit form is forwarded to the server without a read. A wrong slug is therefore rejected by the server as a generic server error (exit code 1), rather than as a local usage error.
alter managed-secrets delete <secret-id> --confirm openai-apiaccess
Section titled “access”Compute the effective access for a secret — every principal that can reach it, whether directly, through a group, as a system principal, or via an agent. Use --output json to keep the full nested envelope with per-path counts.
--limit accepts 1–1000 (default 200); --offset defaults to 0.
alter managed-secrets access <secret-id> --output jsonset-delegation-policy
Section titled “set-delegation-policy”Set the user → agent delegation policy on a managed secret — controls whether members of a group-typed grant may delegate the secret to an agent, caps a delegation’s lifetime, and sets the operator ceiling that gates onward agent → agent delegation chains. Needs dashboard_secrets:write.
| Flag | Description |
|---|---|
--allow-group-delegation | Allow members of a group-typed grant to delegate this secret to an agent. |
--no-allow-group-delegation | Stop members of a group-typed grant delegating this secret to an agent. Mutually exclusive with --allow-group-delegation. |
--max-delegation-ttl-days <days> | Cap on a single delegation’s lifetime, 1–1825 days. |
--delegable | Developer ceiling: allow grants under this secret to be marked re-delegable, gating onward agent → agent delegation for the whole secret. |
--no-delegable | Developer ceiling: refuse onward agent → agent delegation for the whole secret. Mutually exclusive with --delegable. |
--max-delegation-depth <n> | Max delegation-chain depth under this secret, 0–8 (global cap is 8). |
--output <format> | json (default), jsonl, or table. |
Pass at least one flag. The command sends only the flags passed, and Alter
keeps the stored value of every other setting, so nothing is turned off or
reset by omission, and two operators changing different settings at the same
time do not undo each other. A secret with no policy yet starts from group
delegation off, a 90-day TTL ceiling, onward delegation off, and the global
depth cap of 8. The CLI refuses conflicting flags, a run with nothing to
change, and an out-of-range number with a usage error (exit 2) before sending
anything. The command prints the updated secret; its delegation_policy field
is the stored policy. The OAuth provider counterpart is
alter providers set-delegation-policy.
# Enable group delegation with a 30-day capalter managed-secrets set-delegation-policy <secret-id> --allow-group-delegation --max-delegation-ttl-days 30
# Allow onward agent → agent delegation up to 3 hops deepalter managed-secrets set-delegation-policy <secret-id> --delegable --max-delegation-depth 3
# Change only the TTL ceiling (everything else stays as it is)alter managed-secrets set-delegation-policy <secret-id> --max-delegation-ttl-days 60
# Turn group delegation back offalter managed-secrets set-delegation-policy <secret-id> --no-allow-group-delegationset-allowed-hosts
Section titled “set-allowed-hosts”Restrict which destination hosts a secret may be injected into when proxied — an egress allowlist that bounds where the credential can be sent. Needs dashboard_secrets:write.
| Flag | Description |
|---|---|
--host <host> | Allowed host: exact (api.stripe.com) or leading-wildcard subdomain (*.stripe.com). Repeatable, or comma-separated; both forms accumulate. Must be a bare host — no scheme, port, path, @, whitespace, or the URL delimiters ? # % \. Each label is limited to 63 characters and the whole name to 253 (251 for a wildcard suffix, since a wildcard always matches at least one more label). |
--clear | Remove all configured allowed hosts. With default enforcement, proxy calls for the secret are blocked until hosts are configured again. Mutually exclusive with --host. |
# Restrict to Stripe onlyalter managed-secrets set-allowed-hosts <secret-id> --host api.stripe.com --host '*.stripe.com'
# Clear all allowed hosts (proxy calls are blocked until hosts are configured)alter managed-secrets set-allowed-hosts <secret-id> --clearusers / groups
Section titled “users / groups”Picker helpers for building grants — list the app’s users and groups, with substring search.
alter managed-secrets users --search alicealter managed-secrets groups list --idp <idp-id>alter managed-secrets groups show <group-id>users and groups list accept --idp <uuid>, --search (up to 255
characters), --limit (1–200, default 50), and --offset (default 0). users
also accepts --group <uuid> to list only the active members of one group (the
id from groups list); pass one of them as --user to alter policy simulate
to explain a grant that is held by the group.
groups show paginates members and
grants independently with --member-limit / --member-offset and
--grant-limit / --grant-offset; both limits default to 100 and accept
1–500.
grants
Section titled “grants”Grants hand a secret to a principal. Revoking a grant is a soft-delete (recoverable by re-granting). Create and update need dashboard_secrets:write; revoke needs dashboard_secrets:admin — the same recoverable-revoke tier as API keys, agents, and OAuth grants, so a routine :write CI token can’t cut off live principals.
Both grant list commands accept --limit 1–1000 (default 100) and --offset
(default 0).
alter managed-secrets grants list <secret-id>alter managed-secrets grants list-for-agent <agent-id>alter managed-secrets grants get <grant-id>alter managed-secrets grants create <secret-id> --principal-type <type> [...]alter managed-secrets grants update <grant-id> [--label ...] [--expires-at ...]alter managed-secrets grants revoke <grant-id> [--yes]Both list commands show where each grant sits in the delegation tree:
| Column | Meaning |
|---|---|
DEPTH | Hop distance from the secret’s root grant: 0 is a root grant created directly on the secret (from the dashboard, this CLI, or the application’s own backend), 1 a first-hop delegation to an agent or another user, and 2+ an onward agent→agent delegation. ? means the backend did not report the field. |
PARENT | The grant a delegated one was minted under, or — for a root. Shown on list-for-agent only — see below. |
Together these separate shared access from access the grant holds in its own
right. In --output json / jsonl the same data appears as the depth and
parent_grant_id fields on both commands — parent_grant_id (null for a
root) lets scripts reconstruct the whole delegation tree from the flat list.
list <secret-id> renders DEPTH but not PARENT. A parent on that listing
is normally another row of the same table, and a second full grant id beside
GRANT_ID pushes the row past a typical terminal width; use --output json
when you need the edge itself.
list-for-agent adds PARENT, because there it points to a grant that is
not one of the rows: the listing is scoped to a single agent, so the
parent — the delegating user’s grant on a first hop, or the delegating
agent’s on an onward hop — is never shown alongside it. Resolve the id with
alter managed-secrets grants get <id>. A parent that was already revoked
does not resolve (revoked grants are filtered from reads); a child still
listed under one means the revocation did not cascade, and the child should be
revoked directly.
grants create
Section titled “grants create”The principal type drives which flags are required:
--principal-type | Required flags |
|---|---|
user | --app-user-id <uuid>, --label <string> |
group | --idp-id <uuid>, --external-group-id <id>, --label <string> |
agent | --agent-id <uuid> |
system | — |
Optional on every type: --description, --expires-at <iso>, and
--grant-policy <@file.json> for the full policy shape. The policy flag
requires a path-prefixed JSON object; when both policy input and --expires-at
specify an expiry, --expires-at wins. Or supply the whole body with
--input.
alter managed-secrets grants create <secret-id> \ --principal-type agent --agent-id <agent-id> --expires-at 2026-12-31T00:00:00Z--external-group-id takes the IDP’s stable group identifier, never a display name — an Okta group id (00g…), or a Clerk / WorkOS organization id (org_…):
# Okta groupalter managed-secrets grants create <secret-id> \ --principal-type group --idp-id <idp-id> \ --external-group-id 00g8xk2mnpQRS7Vw1697 --label "Data science team"
# WorkOS organizationalter managed-secrets grants create <secret-id> \ --principal-type group --idp-id <idp-id> \ --external-group-id org_01HXYZABCDEF123456789 --label "Acme Corp"Approver validation
Section titled “Approver validation”When a grant policy enables human-in-the-loop approval (--grant-policy on grants create / grants update, or a grant_policy in a create --input body), the approver list is validated at save time:
- An enabled
requires_approvalwith an emptyapproverslist is rejected locally (exit code2, before any network call): managed-secret grants have no default approver, so that configuration could only ever fail at first use. The backend enforces the same rule with a structured 422 (approval_config_error). - An approver the directory definitively does not contain fails the save with a structured 422 — fix the email and retry.
- An approver the directory couldn’t verify (no management credential configured, a transient directory error) allows the save and prints a warning on stderr:
alter: approver not verified — … (save was allowed). Confirm the address is correct — an unreachable approver silently stalls every approval it gates.
grants update
Section titled “grants update”Edit --label, --description, --expires-at, or --grant-policy (at least one required), or supply --input.
alter managed-secrets grants update <grant-id> --label "renamed"--grant-policy replaces the grant’s settings-owned policy block as a
whole — its TTL, approval requirement, and method/endpoint restrictions. It
never touches policy rules authored separately on the same grant (via
alter policy rules …, the dashboard’s Runtime policies section, or the wallet):
those are independent objects and survive every settings save. The read side
is symmetric: grants get, grants list, and update/create responses show
the currently enforced settings block on grant_policy
(requires_approval / restrictions), so what you read back is what the
gate enforces.
--expires-at without --grant-policy is a TTL-only edit: the CLI reads
the grant back (grants get), merges the new expiry into the currently
enforced block, and saves the merged block — the approval requirement and
restrictions are preserved, exactly like the dashboard editor. Combined with
--grant-policy, the expiry overrides that file’s expires_at and the file
otherwise replaces the block as described above.
grants get
Section titled “grants get”alter managed-secrets grants get <grant-id>Fetches one grant. Requires dashboard_secrets:read. grant_policy carries
the currently enforced settings block, projected from the enforcing
rules — the read half that makes safe read-modify-write scripting possible.
grants revoke
Section titled “grants revoke”alter managed-secrets grants revoke <grant-id> # prompts y/Nalter managed-secrets grants revoke <grant-id> --yes # CITable output prints Revoked grant <id>.; JSON and JSONL output emit
{"grant_id":"<id>","status":"revoked"}.
Recipes
Section titled “Recipes”Provision a backend credential in CI
printf '%s' "$OPENAI_KEY" | alter managed-secrets create \ --name "OpenAI" --template openai --credential-value -Grant an agent access, then verify it resolved
alter managed-secrets grants create <secret-id> --principal-type agent --agent-id <agent-id>alter managed-secrets access <secret-id> --output json | jq '.entries'Related
Section titled “Related”alter agents— the agent principals you grant toalter audit grant-events— see grant lifecycle in the log- Managed secrets, Delegation