Skip to content

Reference

alter managed-secrets

Manage managed-secret providers, the credentials they hold, 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 providers themselves, the credentials (keys) each one holds, the grants that hand a credential to principals, and the helpers that answer “who can reach this provider.”

Each entry you create is a per-app provider (its template, injection rules, allowed hosts and delegation policy). It holds one or more credentials — a developer’s keys, such as separate production and staging keys for the same API, and the keys end users store for themselves. Every grant serves exactly one credential, and a delegation always uses its parent grant’s credential.

Every provider has an id and a slug, and several providers may share a display name or a template:

  • The commands on this page take the provider’s slug or id: <provider> is the slug field or the id field (the SLUG and ID columns of list). A slug is 1–50 lowercase letters, digits and hyphens; a value that is neither a slug nor a UUID exits 2 before any request. Alter never generates a UUID-shaped slug, so a UUID is read as the id. A slug is matched exactly, case included, among the app’s active providers. When no active provider in the app has that slug, the command changes nothing, exits 4, and prints:

    alter: no provider with slug "stripe-2" in this app (run alter managed-secrets list)

    Looking up a slug reads the app’s providers, so it also needs dashboard_secrets:read (without it the command exits 7). A provider created before Alter stopped generating UUID-shaped slugs may still hold one, so an id is first checked against the same read: when an active provider has that value as its slug, the command refuses it as ambiguous, exits 2, and names that provider’s id. A token without dashboard_secrets:read skips the check and passes the id.

  • Runtime calls name a provider by its slug: the slug field, the SLUG column of list. It is what the SDK takes as provider. It is generated from the name, unique among the app’s active providers, with -2, -3, … appended when the base slug is taken. delete --confirm also takes the slug.

  • A template id such as openai only picks the injection format, host rules and setup copy. It never names a provider.

alter managed-secrets templates
alter managed-secrets list
alter managed-secrets show <provider>
alter managed-secrets create (--input <path> | --name <name> [--credential-value <value> | --credentials <@file.json>] [--credential-label <label>]) [...]
alter managed-secrets rotate <provider> (--input <path> | --credential-value <value> | --credentials <@file.json>) [--credential-id <credential-id>] [--template <template-id>] [--skip-preflight] [--confirm-template-migration <slug>]
alter managed-secrets credentials list | add | rotate | rename | delete
alter managed-secrets delete <provider> [--confirm <slug>]
alter managed-secrets access <provider>
alter managed-secrets set-delegation-policy <provider> [--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 <provider> --host <host> [--host ...] | --clear
alter managed-secrets set-end-user-key-setup <provider> [--scope <scope> ...] [--instructions <text>] [--binding-host <host> ...] [--clear-scopes] [--clear-instructions] [--clear-binding-hosts] | --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 / set-end-user-key-setup / grant create + update need dashboard_secrets:write; grants revoke needs dashboard_secrets:admin (the revoke tier, matching grants revoke); delete and credentials delete need dashboard_secrets:delete. credentials list needs dashboard_secrets:read; credentials add, rotate and rename need dashboard_secrets:write.

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 <provider> # never exposes a credential value

list accepts --limit 1–1000 (default 100), --offset (default 0) and --search <substring>, a case-insensitive match on the provider’s name or slug (up to 255 characters), so one provider can be found by name in an app that holds more providers than one page.

list prints ID (the id field) and SLUG (the slug field, which the SDK takes as provider); the other commands on this page take either one as <provider>. The slug is printed in full, never truncated. Two providers can share a NAME; the slug and the id tell them apart.

list also prints CREDENTIALS, the number of developer credentials the provider holds (the developer_credential_count field), and USER_KEYS, the number of end users who stored their own key for it (the end_user_credential_count field); each is — against a backend that does not report it. show returns the same fields, and show and list --output json also carry the provider’s end-user key setup: end_user_key_scopes, end_user_key_instructions, binding_host_patterns, and the template’s scope catalog, template_api_key_scopes.

Both print UPDATED alongside CREATED. UPDATED is the last write to the provider — a developer credential’s rotation, addition or deletion, or a metadata edit such as the delegation policy or the allowed-host list (an end user’s own key does not move it) — 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 provider. With a key (--credential-value, or a credential_value in --credentials), the same call stores it as the provider’s first developer credential and creates its base grant; the response carries the new credential’s credential_id and credential_label fields. Without a key, it creates a provider that holds no credential and no grant yet — see creating without a credential. --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 among the app’s active providers. Names need not be unique: a second provider named Stripe gets the slug stripe-2, so read the slug from the response instead of deriving it from the name. The slug is the stable handle runtime calls use: the SDK’s provider, Connect sessions, and the SDK’s grant filters. A stale request body that still ships a slug field is silently ignored.

The new provider’s slug is the response’s slug field — pass it as provider in the SDK — and its id is the id field; the other commands on this page take either one. On stderr the CLI prints the generated slug and names both fields, for example:

alter: created provider "Stripe" with slug stripe-2. The slug is in the `slug` field: pass it as `provider` in the SDK. The managed-secrets commands take the slug or the `id` field.
FlagDescription
--name <name>Display name; need not be unique. The slug is generated from it. Required in per-field mode; omitted when --input supplies the body.
--description <text>Free-form description.
--template <id>Template ID (e.g. openai): the injection format and host rules, never the provider’s address. Omit for a custom provider.
--credential-type <type>bearer_token (default), header_key, basic_auth, or aws_sig_v4.
--credential-value <value>Primary credential. Omit it (with --credentials holding no credential_value) to create a provider with no credential. See credential intake.
--credential-label <label>Name of the first developer credential (default Default), up to 255 characters. Labels are unique, ignoring case, among the provider’s active developer credentials. Requires a key.
--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 base grant.
--account-display-name <name>Optional account display name metadata.
--allow-group-delegationAllow members of a group-typed grant on this provider to delegate it 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 provider to be marked re-delegable, gating onward agent → agent delegation for the whole provider (default: off).
--max-delegation-depth <n>Max delegation-chain depth under this provider, 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 -

Omit --credential-value to create the provider first and store keys later with credentials add. Such a provider has no grant, so nothing can use it until a credential is added and a grant is created for it. The flags that describe a key or its base grant (--credentials, --credential-field, --credential-label, --label, --account-identifier, --account-display-name, --skip-preflight) need a key; the CLI rejects them with a usage error (exit 2) instead of sending a body the backend would refuse.

Terminal window
alter managed-secrets create --name "OpenAI" --template openai
printf '%s' "$OPENAI_PROD_KEY" | alter managed-secrets credentials add <provider> \
--label Production --credential-value -

Omitting --template creates a custom provider 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 providers derive no allowed hosts — set the egress allowlist explicitly,
# or proxied calls for the provider are blocked (see set-allowed-hosts below)
alter managed-secrets set-allowed-hosts <provider> --host billing.internal.example.com

The credential preflight always reports skipped for a custom provider — there is no known vendor 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 provider’s slug. 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 providers cannot be converted in place; create a separate Management provider. 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. When the reclassification happens, the response’s template_migration field names the old and new template (from_template_id, to_template_id), and the CLI prints alter: template corrected — <message> on stderr.

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 provider is custom (templateless) and has no vendor 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 a stored developer credential while the grants it serves keep working: the credential keeps its id, only its key changes. Same credential-intake forms and precedence as create: provide --credential-value, a credential_value in --credentials, or an --input body containing credential_value.

Without --credential-id, rotate targets the provider’s only developer credential. Once a provider holds several, pass --credential-id <credential-id> (an id from credentials list); without it the backend refuses with credential_id_required and the CLI prints the choices, one <id> <label> per line, and exits 5. credentials rotate is the same operation with the credential named positionally. An end user’s own key is theirs, so rotating it is refused (credential_user_owned).

With --credential-id, --template remains a local precondition only (the per-credential request does not carry one), so an --input body that contains expected_template_id is rejected; pass --template instead.

The CLI reads the template catalog to apply the same required-field and pattern checks as the dashboard. It also reads the target provider 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 reading the target provider (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 provider bound to a template the catalog no longer lists (a deprecated template) still rotates: the CLI notes that local field validation was skipped and the server validates against the provider’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 template change returns conflict instead of applying validation for the wrong template.

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 stored credential 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 provider. It does not weaken the server’s template, contract-version, or credential-class checks.

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

The credentials (keys) a provider holds. Each grant serves one of them.

alter managed-secrets credentials list <provider> [--kind <kind>] [--status <status>]
alter managed-secrets credentials add <provider> --label <label> (--credential-value <value> | --credentials <@file.json>) [--credential-field <k=v> ...] [--skip-preflight]
alter managed-secrets credentials rotate <provider> <credential-id> (--input <path> | --credential-value <value> | --credentials <@file.json>) [...]
alter managed-secrets credentials rename <provider> <credential-id> --label <label>
alter managed-secrets credentials delete <provider> <credential-id> [--confirm <label>]
Terminal window
alter managed-secrets credentials list <provider>
alter managed-secrets credentials list <provider> --kind end_user --output json

Lists metadata only — never a key. --kind is developer or end_user (the keys end users store for themselves); --status is active (the default), deleting or revoked. --limit accepts 1–200 (default 50); --offset defaults to 0.

ColumnJSON fieldMeaning
IDidThe credential id that grants create --credential-id, rotate --credential-id and the other credentials commands take.
KINDkinddeveloper or end_user.
LABELlabelOperator-facing name.
STATUSstatusactive, deleting or revoked.
PREVIEWcredential_previewA masked preview of a developer key; — for an end user’s key.
OWNERownerThe end user who owns the key (their email); — for a developer credential.
USER_OWNEDuser_ownedyes for an end user’s own key: you can delete it (credentials delete) and nothing else, since rotating or renaming it is refused with credential_user_owned (exit 7). ? means the backend did not report it.
GRANTSgrant_countActive grants served by the credential.
PREFLIGHTpreflight_statusThe last save-time preflight verdict.
ROTATED / CREATEDrotated_at / created_atTimestamps.

--output json keeps the full envelope (credentials, total, limit, offset, has_more) and every field, including owner.app_user_id, requested_scopes, allowed_hosts, preflight_message, last_used_at and revoked_at. A credential’s allowed_hosts is set only for a template that derives its hosts from the key; for every other template it is null and the provider’s own allowed hosts apply to each credential.

Store an additional developer credential — a second key for the same API, such as a staging key next to production. It issues no grant: assign it with grants create --credential-id. The new id is the credential.id field of the response, and the CLI prints the follow-up command on stderr.

FlagDescription
--label <label>Required. Up to 255 characters, no control characters, unique (ignoring case) among the provider’s active developer credentials; a duplicate is refused with credential_label_conflict.
--credential-value <value>The key, in any credential intake form. Required unless --credentials contains credential_value.
--credentials <@file.json>Multi-field credentials as a JSON object; takes precedence over --credential-field.
--credential-field <k=v>Repeatable; a form_schema field of the provider’s template.
--skip-preflightStore without the provider check (audited); the response’s preflight summary reports skipped.
--output <format>json (default), jsonl, or table.

The CLI reads the provider’s template and checks the key the way create does. A new credential inherits nothing, so every required template field must be supplied (unlike rotate, which keeps stored text fields). The preflight verdict prints on stderr. A Supabase or Twilio provider whose stored credential still needs its contract repaired refuses a second credential (provider_contract_repair_required); rotate the existing one first.

Terminal window
printf '%s' "$OPENAI_STAGING_KEY" | alter managed-secrets credentials add <provider> \
--label Staging --credential-value - --output json | jq -r '.credential.id'
Terminal window
printf '%s' "$NEW_KEY" | alter managed-secrets credentials rotate <provider> <credential-id> --credential-value -

The same operation, flags and checks as rotate --credential-id: the credential keeps its id and every grant it serves keeps working.

Terminal window
alter managed-secrets credentials rename <provider> <credential-id> --label "Production (EU)"

Renames a developer credential, with the same label rules as credentials add. An end user’s key cannot be renamed or rotated: the CLI prints credential <credential-id> is an end user's own key (credential_user_owned) with the credentials delete command you can run instead, and exits 7.

Gated behind dashboard_secrets:delete and a type-to-confirm check against the credential’s label:

  • Interactive: the CLI looks the credential up, prints how many grants the deletion revokes, and prompts for the label. This path also needs dashboard_secrets:read.
  • Non-interactive (CI): pass --confirm <label>. There is no --yes shortcut, so a script must name the credential it destroys. A wrong label is refused by the server (confirm_name_required, exit 1).
Terminal window
alter managed-secrets credentials delete <provider> <credential-id> --confirm Staging

Prints Deleted credential <id> (<label>); revoked <n> grant(s). — <n> is the response’s revoked_grant_count field.

Like apps delete, it’s gated behind the separate dashboard_secrets:delete scope and a type-to-confirm check against the provider’s slug (<provider> is its slug or id; --confirm takes its slug, whichever form names the provider):

  • Interactive: the CLI prompts for the slug. This path also needs dashboard_secrets:read — it fetches the provider 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 and name the provider by its id, since looking up a slug reads the app’s providers.

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 <provider> --confirm openai-api

Before the delete call the command prints Deleting provider <id> (<slug>) on stderr, and on success Deleted provider <id>. on stdout. If the delete stops part way (a server error such as a 503 while the stored credentials are removed, or a lost connection), the provider is left marked for deletion: it no longer appears in list, its slug no longer resolves, and a new provider may take that slug. The error message says so; retry by the id from the Deleting provider line, never by the slug:

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

A slug that no active provider holds exits 4, and for delete the message says to retry a part-way delete by that id.

Compute the effective access for a provider — 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.

The CREDENTIAL column is the credential each access path runs through (the entry’s credential_id field; --output json adds credential_kind). ? means the backend did not report it.

Terminal window
alter managed-secrets access <provider> --output json

Set the user → agent delegation policy on a provider — controls whether members of a group-typed grant may delegate it 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 on this provider to delegate it to an agent.
--no-allow-group-delegationStop members of a group-typed grant on this provider delegating it 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 provider to be marked re-delegable, gating onward agent → agent delegation for the whole provider.
--no-delegableDeveloper ceiling: refuse onward agent → agent delegation for the whole provider. Mutually exclusive with --delegable.
--max-delegation-depth <n>Max delegation-chain depth under this provider, 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 provider 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 provider; 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 <provider> --allow-group-delegation --max-delegation-ttl-days 30
# Allow onward agent → agent delegation up to 3 hops deep
alter managed-secrets set-delegation-policy <provider> --delegable --max-delegation-depth 3
# Change only the TTL ceiling (everything else stays as it is)
alter managed-secrets set-delegation-policy <provider> --max-delegation-ttl-days 60
# Turn group delegation back off
alter managed-secrets set-delegation-policy <provider> --no-allow-group-delegation

Restrict which destination hosts a provider’s credentials 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 provider are blocked until hosts are configured again. Mutually exclusive with --host.
Terminal window
# Restrict to Stripe only
alter managed-secrets set-allowed-hosts <provider> --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 <provider> --clear

Set what end users are shown when they store their own key for the provider on the hosted Connect page: the vendor scopes the key needs, instructions, and, for a bound template, the hosts the key may be bound to. Needs dashboard_secrets:write. The command changes configuration only and can be run again to change it back; no key, grant, or delegation is touched.

FlagDescription
--scope <scope>A vendor scope the end user’s key needs. Repeat the flag for each scope (a value is never split on commas, because a vendor scope may contain one). Up to 100 scopes, each up to 128 printable characters with no whitespace, and together under 4,000 bytes as JSON; a repeat is ignored. When the template has a scope catalog (the provider’s template_api_key_scopes field), only its id values are accepted. The scopes are shown to the end user as guidance; Alter does not verify them against the vendor.
--clear-scopesRequire no scopes.
--instructions <text>Plain-text instructions shown before the key field: the text itself, @/path/to/file, or - to read stdin. Newlines and tabs are kept (Windows and old Mac line endings are read as plain newlines), no other control character is accepted, and surrounding whitespace is trimmed; 1–4,000 characters.
--clear-instructionsShow no instructions. An empty --instructions is refused instead, so an unset shell variable can never clear them.
--binding-host <host>Bound templates only (a template whose BINDING column in templates names a field, such as a deployment URL): a host the key’s derived host must match, exact (acme.example.com) or leading-wildcard (*.example.com), checked when a user stores a key and again on every call made with it. Repeatable or comma-separated, with the same grammar, normalization, and limits as set-allowed-hosts --host.
--clear-binding-hostsBound templates only: remove the patterns. Users can then store no key, and keys already stored stop working until a pattern allows their host again.
--clearClear the whole setup: no scopes, no instructions, no binding hosts. Cannot be combined with any other setup flag.
--output <format>json (default), jsonl, or table.

Each flag replaces its whole field (a new --scope list replaces the stored list), and a field you do not name keeps its stored value: the command reads the provider first, as the dashboard’s setup card does before it saves, and sends the stored value back for every field you did not name. Pass at least one flag. The CLI refuses, with a usage error (exit 2) before saving anything, a run with nothing to change, a field flag combined with the flag that clears the same field, --clear combined with anything else, a scope outside the template’s catalog, a malformed host, and --binding-host or --clear-binding-hosts on a template that is not bound.

The server applies the same rules. When it refuses a field the template cannot take (end_user_key_setup_invalid, with unknown_scope for a scope outside the catalog and template_not_bound for binding hosts on a template that is not bound), the CLI prints the same message as its local check, names both codes, and also exits 2.

The command prints the updated provider. Its end_user_key_scopes, end_user_key_instructions, and binding_host_patterns fields are the stored setup (a cleared field reads null), and template_api_key_scopes lists the scopes the template’s keys offer (id and description; an empty list for a template with no catalog, whose scopes are free-form).

Terminal window
# Ask for two scopes and explain where the key is created
alter managed-secrets set-end-user-key-setup <provider> \
--scope employees:read --scope companies:read \
--instructions @./key-instructions.txt
# A bound template: only keys for the company's own deployments
alter managed-secrets set-end-user-key-setup <provider> --binding-host '*.acme.example.com'
# Change the instructions only (scopes and binding hosts stay as they are)
printf '%s' "$INSTRUCTIONS" | alter managed-secrets set-end-user-key-setup <provider> --instructions -
# Remove everything
alter managed-secrets set-end-user-key-setup <provider> --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 provider’s credential 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.

A grant can be user-owned: the grant of an end user’s own key and every delegation under it, and any delegation a user consented to (from a developer credential too). As with a user’s OAuth connection, you can revoke it and nothing else: grants update on it is refused with grant_user_owned (exit 7), and so is a grant-level policy rule on it (alter policy rules create --grant, bind --target grant:). The grant’s user_owned field says which.

Both grant list commands accept --limit 1–1000 (default 100) and --offset (default 0). grants list also filters by credential: --credential-kind developer|end_user (a delegation follows its root’s credential) and --credential-id <credential-id> (the grants one credential serves — what credentials delete would revoke).

alter managed-secrets grants list <provider>
alter managed-secrets grants list-for-agent <agent-id>
alter managed-secrets grants get <grant-id>
alter managed-secrets grants create <provider> --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, and whose it is:

ColumnMeaning
CREDENTIALThe credential the grant serves: its label, or its id when it has none. ? means the backend did not report it. In JSON the fields are credential_id, credential_kind and credential_label.
DEPTHHop distance from the provider’s root grant: 0 is a root grant created directly on the provider (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.
USER_OWNEDyes for a user-owned grant, which grants update refuses (JSON field user_owned). ? means the backend did not report it.

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 <provider> 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—

--credential-id <credential-id> names the developer credential the grant serves. It may be omitted while the provider holds exactly one active developer credential (end users’ own keys are not counted). Once it holds several, a create without it is refused with credential_id_required, and the CLI lists the choices (<id> <label>) and exits 5. The other credential refusals are no_developer_credential (the provider holds no key yet — add one with credentials add), end_user_credential_not_assignable (an end user’s key serves only its owner’s grant), credential_not_found (no active credential with that id on this provider) and sibling_label_conflict (the user already holds a grant with that label on this provider).

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 <provider> \
--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 <provider> \
--principal-type group --idp-id <idp-id> \
--external-group-id 00g8xk2mnpQRS7Vw1697 --label "Data science team"
# WorkOS organization
alter managed-secrets grants create <provider> \
--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.

A user-owned grant cannot be edited: the server refuses with grant_user_owned, and the CLI prints grant <grant-id> is user-owned (grant_user_owned) with the grants revoke command you can run instead, and exits 7. On the TTL-only path, where the CLI has already read the grant, it refuses from the grant’s user_owned field before sending anything, as the dashboard hides the edit controls.

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"}.

Revoking is the one change you can make to a user-owned grant. Revoking the grant of an end user’s own key also revokes the key and destroys it, with every delegation under it; the user stores a key again through Connect.

On a terminal, before the y/N prompt, the CLI reads a user-owned grant and prints what the revoke takes with it, as the dashboard’s revoke dialog does: that the user’s key is destroyed (or, for a delegation the user consented to, that the key is kept), and the id and label of every delegation made from it. A grant it cannot read (not found, exit 4, or no dashboard_secrets scope, exit 7) fails before the prompt, as the revoke would. With --yes, or off a terminal, nothing is read first.

Provision a backend credential in CI

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

Run separate production and staging keys for one API

Terminal window
printf '%s' "$STAGING_KEY" | alter managed-secrets credentials add <provider> \
--label Staging --credential-value -
alter managed-secrets credentials list <provider>
alter managed-secrets grants create <provider> --credential-id <staging-credential-id> \
--principal-type agent --agent-id <staging-agent-id>

Grant an agent access, then verify it resolved

Terminal window
alter managed-secrets grants create <provider> --principal-type agent --agent-id <agent-id>
alter managed-secrets access <provider> --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.