Skip to content

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 templates
alter managed-secrets list
alter 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 ...] | --clear
alter managed-secrets users
alter managed-secrets groups list | show <group-id>
alter managed-secrets grants list | list-for-agent | get | create | update | revoke

All 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.

List the managed-secret template catalog for the app (openai, aws, …). Templates pre-fill the credential type and injection rules.

Terminal window
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.

Terminal window
alter managed-secrets list
alter managed-secrets show <secret-id> # never exposes the credential value

list 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 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.

FlagDescription
--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-delegationAllow 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).
--delegableDeveloper 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-preflightSave 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.
Terminal window
# Single-field bearer token, value piped from stdin
printf '%s' "$OPENAI_KEY" | alter managed-secrets create \
--name "OpenAI" --template openai --credential-value -

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):

Terminal window
# Custom header-key API, value piped from stdin
printf '%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.com

The 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 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:

Terminal window
alter managed-secrets create \
--name "Supabase Project APIs" \
--template supabase \
--credential-value @./supabase-project-key \
--credential-field project_url=https://PROJECT_REF.supabase.co

Use an sbp_ personal access token with the Management API template:

Terminal window
alter managed-secrets create \
--name "Supabase Management" \
--template supabase-management \
--credential-value @./supabase-management-pat

The 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.

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.

The primary credential value (--credential-value) accepts three forms, in decreasing order of safety:

FormReads 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/fileA 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:

Terminal window
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.

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.

Terminal window
printf '%s' "$NEW_KEY" | alter managed-secrets rotate <secret-id> --credential-value -

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>. A dashboard_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.

Terminal window
alter managed-secrets delete <secret-id> --confirm openai-api

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.

Terminal window
alter managed-secrets access <secret-id> --output json

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.

FlagDescription
--allow-group-delegationAllow members of a group-typed grant to delegate this secret to an agent.
--no-allow-group-delegationStop 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.
--delegableDeveloper ceiling: allow grants under this secret to be marked re-delegable, gating onward agent → agent delegation for the whole secret.
--no-delegableDeveloper 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.

Terminal window
# Enable group delegation with a 30-day cap
alter managed-secrets set-delegation-policy <secret-id> --allow-group-delegation --max-delegation-ttl-days 30
# Allow onward agent → agent delegation up to 3 hops deep
alter 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 off
alter managed-secrets set-delegation-policy <secret-id> --no-allow-group-delegation

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.

FlagDescription
--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).
--clearRemove all configured allowed hosts. With default enforcement, proxy calls for the secret are blocked until hosts are configured again. Mutually exclusive with --host.
Terminal window
# Restrict to Stripe only
alter 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> --clear

Picker helpers for building grants — list the app’s users and groups, with substring search.

Terminal window
alter managed-secrets users --search alice
alter 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 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:

ColumnMeaning
DEPTHHop 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.
PARENTThe 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.

The principal type drives which flags are required:

--principal-typeRequired 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.

Terminal window
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_…):

Terminal window
# Okta group
alter managed-secrets grants create <secret-id> \
--principal-type group --idp-id <idp-id> \
--external-group-id 00g8xk2mnpQRS7Vw1697 --label "Data science team"
# WorkOS organization
alter managed-secrets grants create <secret-id> \
--principal-type group --idp-id <idp-id> \
--external-group-id org_01HXYZABCDEF123456789 --label "Acme Corp"

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_approval with an empty approvers list is rejected locally (exit code 2, 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.

Edit --label, --description, --expires-at, or --grant-policy (at least one required), or supply --input.

Terminal window
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.

Terminal window
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.

Terminal window
alter managed-secrets grants revoke <grant-id> # prompts y/N
alter managed-secrets grants revoke <grant-id> --yes # CI

Table output prints Revoked grant <id>.; JSON and JSONL output emit {"grant_id":"<id>","status":"revoked"}.

Provision a backend credential in CI

Terminal window
printf '%s' "$OPENAI_KEY" | alter managed-secrets create \
--name "OpenAI" --template openai --credential-value -

Grant an agent access, then verify it resolved

Terminal window
alter managed-secrets grants create <secret-id> --principal-type agent --agent-id <agent-id>
alter managed-secrets access <secret-id> --output json | jq '.entries'

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.