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 theslugfield or theidfield (theSLUGandIDcolumns oflist). A slug is 1–50 lowercase letters, digits and hyphens; a value that is neither a slug nor a UUID exits2before 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, exits4, 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 exits7). 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, exits2, and names that provider’s id. A token withoutdashboard_secrets:readskips the check and passes the id. -
Runtime calls name a provider by its slug: the
slugfield, theSLUGcolumn oflist. It is what the SDK takes asprovider. It is generated from the name, unique among the app’s active providers, with-2,-3, … appended when the base slug is taken.delete --confirmalso takes the slug. -
A template id such as
openaionly picks the injection format, host rules and setup copy. It never names a provider.
alter managed-secrets templatesalter managed-secrets listalter 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 | deletealter 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 ...] | --clearalter managed-secrets set-end-user-key-setup <provider> [--scope <scope> ...] [--instructions <text>] [--binding-host <host> ...] [--clear-scopes] [--clear-instructions] [--clear-binding-hosts] | --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 / 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.
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 <provider> # never exposes a credential valuelist 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
Section titled “create”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.| Flag | Description |
|---|---|
--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-delegation | Allow 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). |
--delegable | Developer 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-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 -Creating without a credential
Section titled “Creating without a credential”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.
alter managed-secrets create --name "OpenAI" --template openaiprintf '%s' "$OPENAI_PROD_KEY" | alter managed-secrets credentials add <provider> \ --label Production --credential-value -Custom providers
Section titled “Custom providers”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):
# 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 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.comThe 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 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 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.
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 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.
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 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.
printf '%s' "$NEW_KEY" | alter managed-secrets rotate <provider> --credential-value -credentials
Section titled “credentials”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>]credentials list
Section titled “credentials list”alter managed-secrets credentials list <provider>alter managed-secrets credentials list <provider> --kind end_user --output jsonLists 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.
| Column | JSON field | Meaning |
|---|---|---|
ID | id | The credential id that grants create --credential-id, rotate --credential-id and the other credentials commands take. |
KIND | kind | developer or end_user. |
LABEL | label | Operator-facing name. |
STATUS | status | active, deleting or revoked. |
PREVIEW | credential_preview | A masked preview of a developer key; — for an end user’s key. |
OWNER | owner | The end user who owns the key (their email); — for a developer credential. |
USER_OWNED | user_owned | yes 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. |
GRANTS | grant_count | Active grants served by the credential. |
PREFLIGHT | preflight_status | The last save-time preflight verdict. |
ROTATED / CREATED | rotated_at / created_at | Timestamps. |
--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.
credentials add
Section titled “credentials add”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.
| Flag | Description |
|---|---|
--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-preflight | Store 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.
printf '%s' "$OPENAI_STAGING_KEY" | alter managed-secrets credentials add <provider> \ --label Staging --credential-value - --output json | jq -r '.credential.id'credentials rotate
Section titled “credentials rotate”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.
credentials rename
Section titled “credentials rename”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.
credentials delete
Section titled “credentials delete”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--yesshortcut, so a script must name the credential it destroys. A wrong label is refused by the server (confirm_name_required, exit1).
alter managed-secrets credentials delete <provider> <credential-id> --confirm StagingPrints Deleted credential <id> (<label>); revoked <n> grant(s). — <n> is the response’s revoked_grant_count field.
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 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>. Adashboard_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.
alter managed-secrets delete <provider> --confirm openai-apiBefore 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:
alter managed-secrets delete <provider-id> --confirm openai-apiA slug that no active provider holds exits 4, and for delete the message
says to retry a part-way delete by that id.
access
Section titled “access”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.
alter managed-secrets access <provider> --output jsonset-delegation-policy
Section titled “set-delegation-policy”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.
| Flag | Description |
|---|---|
--allow-group-delegation | Allow members of a group-typed grant on this provider to delegate it to an agent. |
--no-allow-group-delegation | Stop 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. |
--delegable | Developer ceiling: allow grants under this provider to be marked re-delegable, gating onward agent → agent delegation for the whole provider. |
--no-delegable | Developer 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.
# Enable group delegation with a 30-day capalter managed-secrets set-delegation-policy <provider> --allow-group-delegation --max-delegation-ttl-days 30
# Allow onward agent → agent delegation up to 3 hops deepalter 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 offalter managed-secrets set-delegation-policy <provider> --no-allow-group-delegationset-allowed-hosts
Section titled “set-allowed-hosts”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.
| 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 provider are blocked until hosts are configured again. Mutually exclusive with --host. |
# Restrict to Stripe onlyalter 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> --clearset-end-user-key-setup
Section titled “set-end-user-key-setup”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.
| Flag | Description |
|---|---|
--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-scopes | Require 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-instructions | Show 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-hosts | Bound templates only: remove the patterns. Users can then store no key, and keys already stored stop working until a pattern allows their host again. |
--clear | Clear 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).
# Ask for two scopes and explain where the key is createdalter 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 deploymentsalter 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 everythingalter managed-secrets set-end-user-key-setup <provider> --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 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:
| Column | Meaning |
|---|---|
CREDENTIAL | The 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. |
DEPTH | Hop 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. |
PARENT | The grant a delegated one was minted under, or — for a root. Shown on list-for-agent only — see below. |
USER_OWNED | yes 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.
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 | — |
--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.
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_…):
# Okta groupalter managed-secrets grants create <provider> \ --principal-type group --idp-id <idp-id> \ --external-group-id 00g8xk2mnpQRS7Vw1697 --label "Data science team"
# WorkOS organizationalter managed-secrets grants create <provider> \ --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.
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.
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"}.
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.
Recipes
Section titled “Recipes”Provision a backend credential in CI
printf '%s' "$OPENAI_KEY" | alter managed-secrets create \ --name "OpenAI" --template openai --credential-value -Run separate production and staging keys for one API
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
alter managed-secrets grants create <provider> --principal-type agent --agent-id <agent-id>alter managed-secrets access <provider> --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