Reference
Output & scripting
Output formats, exit codes, workspace linking, environment variables, and shell completion.
The CLI is built to be scripted. Output is machine-parseable, exit codes are a stable contract, and the app a command operates on can be resolved from the environment instead of a flag on every invocation.
Output formats
Section titled “Output formats”Data-returning commands generally take --output; mutation commands that
only print a confirmation line (apps delete, keys revoke, agents revoke,
and similar) do not. Formatted commands accept all three formats:
| Format | Best for | Notes |
|---|---|---|
table | Interactive use | Plain-ASCII, the default for list commands. |
json | Scripts and jq | Pretty-printed. The default for single-object commands. |
jsonl | Line-oriented list pipelines | One JSON object per line — pipe into jq -c / grep / awk. The CLI validates the page before writing it, so an invalid --fields projection never leaves a partial stream. Falls back to json for single-object responses. |
alter apps list --output jsonalter audit list --since 7d --output jsonl | jq -c 'select(.action == "token.retrieved")'Field selection
Section titled “Field selection”Narrow JSON / JSONL output to specific top-level keys with --fields:
alter apps list --fields id,name# [# { "id": "<app-id>", "name": "demo" },# ...# ]The value is comma-separated (spaces around commas are fine; spaces inside a name are rejected as a typo). An unknown field name fails loudly rather than emitting null — so a typo surfaces on the first run. --fields is inert with --output table, since table columns are already a fixed slice.
Exit codes
Section titled “Exit codes”The CLI returns a structured exit code so scripts can branch on the failure mode without parsing stderr. These are a stable contract across releases.
| Code | Name | Meaning |
|---|---|---|
0 | OK | Command succeeded. |
1 | ERROR | Generic runtime failure, including network/SDK errors and backend statuses without a dedicated mapping (for example 400, 422, and 5xx). |
2 | USAGE | Bad flag, argument, or input format. Always paired with a stderr line naming the offending input. |
3 | AUTH | Not signed in, or the PAT was revoked / expired. Fix: re-run alter auth login. |
4 | NOT_FOUND | Resource not found (404), or a referenced local file is missing. |
5 | CONFLICT | A 409 server-state conflict or a 412 stale reviewed version. Inspect current state and review a new change; do not blindly overwrite it. A 409 that only means “busy” is 75 instead. |
6 | RATE_LIMIT | A 429 — retry with backoff. |
7 | FORBIDDEN | The request was refused with a 403: the PAT lacks the required scope, the organization’s plan does not include the feature, or the route is admin-only and the token’s owner is not an organization admin. Fix: re-mint the PAT with broader scopes, upgrade the plan, or have an admin run the command — the printed message says which. The admin check reads the live membership role on every request, so a demotion turns a previously-working script into a 7 without the token changing. |
8 | CANCELLED | A destructive-action prompt was declined or could not read a matching confirmation. Pass the command’s --yes or --confirm <value> flag in CI. An explicit wrong --confirm value is normally USAGE (2) before the request. |
75 | BUSY | A concurrent change held a lock the command needed, so nothing was saved or applied. Fix: retry the same command unchanged, with backoff. After a busy policy apply, do not re-plan or edit the plan file: the reviewed plan is still valid. Returned for policy_plan_busy by policy plan and policy apply. |
75 is EX_TEMPFAIL from sysexits.h. Apart from self-update, which forwards npm’s own exit code when the install fails, a status not in this table does not come from the CLI: for example, Node.js exits 9 when NODE_OPTIONS or a Node flag is invalid. Fix the environment instead of retrying.
Retries
Section titled “Retries”Commands that manage organization resources retry a small set of transient failures themselves before reporting an exit code: at most three attempts per request (two retries). Whether a request is retried depends on its HTTP method, not on what the command is named. The runtime calls made by sdk-passthrough and the runtime checks in verify go through the SDK and are not covered by this policy.
-
GET requests (
list,show,status, and most other reads) are retried on HTTP 502, 503, or 504, and on any connection-level failure (reset, refused, DNS). A request that times out is not retried: it already waited the full per-request timeout. -
PUT, PATCH, and DELETE requests (most updates, deletes, and
revokecommands) are retried only on a 503 whose error code istransient_db_conflict,lock_contention, ordatabase_unavailable. The API sends the first two when a lock conflict aborted a database transaction; only the failing transaction was rolled back, so an earlier step of the same request may already have taken effect. It sendsdatabase_unavailablewhen it lost its database connection during the request, for example during a database failover; if the connection was lost while a change was being saved, that change may or may not have been saved. Re-sending is still safe because these requests set state rather than add to it. Any other 5xx, or a dropped connection, is reported unretried. Because an earlier attempt may already have applied the change, a 404 or 409 on the retry can be the result of that attempt (for example, the resource is already deleted) as well as of another client; the error message says so. -
POST requests are never retried, so a transient failure can never issue a second one-time secret or a duplicate resource. This covers every create and mint (
create,keys mint,keys rotate,agents mint-key), and also these commands that send a POST:- reads:
policy simulate --candidate,identity-providers discover,audit integrity-check, and the preflight check thatkeys mintruns before minting; - updates:
apps archive,apps unarchive,grants revoke, andidentity-providers webhook enable,disable, androtate.
A POST that fails with a 503
database_unavailablemay or may not have been applied, because the connection can be lost after the change was saved. Before re-running it, check its result: list or show the resource acreatewould have made, and afterkeys mint,keys rotateoragents mint-keylook for the new key and revoke any you do not keep. - reads:
-
Rate limits (429) are never retried; the command exits
6and the script owns the back-off. -
policy planandpolicy applyare POSTs, so they are never retried automatically. When another policy change holds a lock they exit75with nothing saved or applied, and the script owns the retry of the same command. Re-runningplanis safe because the refused attempt saved nothing, and--savewrites the file only after the server saved the plan. Re-runningapplyis safe because apply is idempotent by plan: a plan that already committed returns its committed result.
When the response carries a Retry-After header, the CLI waits that long, capped at 15 seconds, plus up to 20% random jitter — so a single wait is at most 18 seconds. The database 503s (transient_db_conflict, lock_contention, database_unavailable) send Retry-After: 2; the 503 sent while credential verification is temporarily unavailable (error service_unavailable) sends Retry-After: 15, which the cap honours in full. Without the header it waits a random delay of up to 0.5 seconds before the first retry and up to 1 second before the second. The waits for one request never total more than 20 seconds: a retry that would exceed that is skipped and the last response is reported.
Each retry prints one line to stderr, for example alter: GET /apps returned HTTP 503 (transient_db_conflict); retrying in 2.2s (attempt 2 of 3). Stdout is unaffected, so --output json stays parseable. Exit codes are unchanged: a request that still fails after its retries exits with the code in the table above (a 5xx is 1).
Probing the command surface
Section titled “Probing the command surface”Help and version output never report success for a command path that does not
exist. A guessed path is a usage error in every form it can be typed, and all
three forms produce the same stderr and the same exit 2:
alter connect --help # error: unknown command 'connect'alter help connect # error: unknown command 'connect'alter apps nosuchsub --help # error: unknown command 'nosuchsub'alter help apps nosuchsub # error: unknown command 'nosuchsub'alter help <path> resolves the whole path, so it documents the command you
named rather than the namespace it lives in: alter help apps list prints the
same page as alter apps list --help.
An invocation that names no command to run is incomplete: bare alter, and a
bare namespace such as alter apps or alter policy rules. Each writes usage
to stderr and exits 2. Exit 0 in this CLI means a command ran and
succeeded, so an invocation that ran nothing does not report it. Some other
CLIs exit 0 here; if a script relied on that, probe with alter --help or
alter apps --help, which are successful discovery and exit 0 with the help
text on stdout.
Operands beyond a command’s declared arity are a usage error too, rather than being silently discarded.
-V and --version report the CLI’s own version and are accepted only before
a command name. Typed after one they are a usage error, so a version flag can
never pre-empt the command it was typed after:
alter --version # 0.7.3alter apps list --version # error: unknown option '--version'Help discovery and the usage verdict
Section titled “Help discovery and the usage verdict”Asking for help validates what you supplied, and does not require what you
omitted. Every token you did type is checked exactly as it would be without the
help flag: an unknown command, an unknown option, a misspelled subcommand,
conflicting options, and surplus operands all produce the same exit code and
byte-identical stderr with and without --help, including the error: prefix
and the “Did you mean” suggestion.
Three things behave differently on the help path, by design:
- Omitted input is not required.
alter apps show --helpexits0even with<app-id>omitted, andalter providers show --helpexits0without--provider, because those are the values the caller is asking about. Without the help flag both exit2. - A help flag in a value position is an ordinary token. Nothing is
special-cased, so a preceding option claims it:
alter keys list --app --helpruns the command with--appset to the literal string--help. - Validation inside a command’s action does not run. A value check on a
flag is part of executing the command, so
alter apps list --output bogusexits2whilealter apps list --output bogus --helpprints help and exits0.
To explore safely, start at alter --help and descend through confirmed
names, or use alter help <path> for any depth.
managed-secrets delete is the exception for an explicit confirmation: the
CLI can avoid the read needed to discover the secret slug, so it forwards
--confirm and a mismatch returns generic server error code 1.
branding set without --replace, against an app that has no branding yet,
exits 2 (usage) rather than 4: the backend’s 404 carries the machine
code branding_not_configured, which is a fixable input problem — re-run with
--replace — not an absent resource. A script branching on 4 to mean
“create it instead” will never fire.
apps create --stub, against a deployment without stub-provider mode enabled,
exits 2 (usage) rather than the generic 1 a 422 normally maps to, for the
same reason: the rejection is a fixable input problem — re-run without
--stub — not a transient failure. Retrying the identical command can never
succeed, so a script branching on 1 to mean “retry” must not fire here.
sdk-passthrough request is the one command that maps a backend failure to
5 rather than its usual blanket 1: when the grant’s application is a stub
application and the deployment does not run stub-provider mode, the call is
refused permanently. The distinct code exists so a script can tell “this
application can never work against this deployment” from “that one call
failed” — the first must stop, the second may retry.
The pre-minted-token probe in auth login, the remote probe in auth status,
and pats whoami use generic code 1 when the backend rejects a resolved PAT.
Regular resource commands that use the shared authenticated-command path map a
backend 401 to 3.
alter verify also uses code 1 for its verification-input failures: a
malformed --key, a malformed --grant used with --runtime, or a missing
or malformed --agent required by an agent-runtime design.
alter link predates the dedicated cancellation code: declining its
overwrite prompt currently returns 1, and --force is the non-interactive
form. All destructive resource prompts use 8.
Example — probe whether an app exists without erroring on the not-found case:
alter apps show "$APP_ID" --output json > /dev/null 2>&1status=$?if [ "$status" -eq 0 ]; then echo "app exists"elif [ "$status" -eq 4 ]; then echo "app not found"elif [ "$status" -eq 7 ]; then echo "PAT lacks dashboard_apps:read — re-mint with broader scopes"else echo "unexpected error" && exit 1fiCapture $? into a variable immediately — every command clobbers it.
Environment variables
Section titled “Environment variables”| Variable | Used by | Effect |
|---|---|---|
ALTER_PAT | All commands | The PAT to authenticate with. Highest-precedence credential source. |
ALTER_APP_ID | App-scoped commands | Default app ID when --app is omitted (UUID only — the env var and workspace pin never take a name). |
ALTER_API_KEY | sdk-passthrough | Runtime API key for the one-off request escape hatch (not a PAT). |
XDG_CONFIG_HOME | Authentication, Fish completion | Moves the plaintext fallback to $XDG_CONFIG_HOME/alter/auth.toml and the Fish install target to $XDG_CONFIG_HOME/fish/completions/alter.fish. |
XDG_CACHE_HOME | Update notifier | Moves the 24-hour update-check cache from ~/.cache/alter/update-check.json. |
XDG_DATA_HOME | Bash completion | Changes the Bash install target to $XDG_DATA_HOME/bash-completion/completions/alter. |
SHELL | Completion | Auto-detects bash, zsh, or fish when --shell is omitted. |
ALTER_NO_UPDATE_NOTIFIER, NO_UPDATE_NOTIFIER, CI | Update notifier | Any non-empty value other than 0 or false disables the update notice. |
ALTER_NO_TELEMETRY, DO_NOT_TRACK | Usage telemetry | 1 or true disables telemetry when the CLI build has telemetry configured. |
Published builds do not emit telemetry unless telemetry has been explicitly
configured. When enabled, events use a random anonymous installation ID stored
at ~/.config/alter/telemetry-id; no user PII is attached. Delivery is
best-effort and bounded, so it cannot change a command’s result.
Workspace linking
Section titled “Workspace linking”If you work primarily on one app, pin it to the current directory tree so app-scoped commands don’t need --app:
cd ~/code/my-productalter link <app-id># alter: pinned app_id=<app-id> in .../.alter/config.yaml# alter: appended `.alter/` to .gitignore so the pin isn't committed.
# From anywhere in this tree, --app is now optional:alter keys listalter agents create --name worker --type service \ --scopes '{"google":["openid"]}'alter policy show-app
# Inspect or clear the pinalter link --statusalter unlinkThe app ID is resolved in this order (highest precedence first):
--app <id>on the command lineALTER_APP_IDenvironment variable- The nearest
.alter/config.yaml, found by walking up from the current directory - Otherwise:
no app selected(exit code 2)
Discovery checks each parent directory through $HOME itself, then stops
before walking above it. Therefore $HOME/.alter/config.yaml intentionally
acts as a user-wide default; configs above the home directory are never read.
The search tests for .alter/config.yaml, not merely an .alter/ directory.
alter link <app-id> validates the UUID and verifies that the app exists
before writing. If the current directory already pins a different app, it
prompts before overwriting; use --force for scripts. In an enclosing git
repository it appends .alter/ to the repository root’s .gitignore.
alter unlink removes only ./.alter/config.yaml in the current directory;
it does not remove a pin discovered from a parent.
Shell completion
Section titled “Shell completion”Generate static completion scripts for bash, zsh, or fish:
# Write the script to the conventional location and print the line to add to the rc filealter completion install
# Or print to stdout to install it yourselfalter completion print --shell zsh >> ~/.zsh/completions/_alterinstall writes the script and prints the line to activate it; it never edits
.bashrc / .zshrc automatically. The shell is auto-detected from $SHELL
unless --shell is passed. Unsupported or undetectable shells return usage
code 2; installation on Windows also returns 2 and directs the operator
to completion print.
| Shell | Default install path |
|---|---|
| Bash | $XDG_DATA_HOME/bash-completion/completions/alter, or ~/.local/share/bash-completion/completions/alter |
| Zsh | ~/.zsh/completions/_alter |
| Fish | $XDG_CONFIG_HOME/fish/completions/alter.fish, or ~/.config/fish/completions/alter.fish |
The generated scripts do not query Alter. They statically complete this
current top-level subset: auth, apps, keys, agents, providers,
policy, audit, pats, link, unlink, completion,
sdk-passthrough, self-update, and help, plus a static subset of their
verbs. They do not complete flags, resource IDs, or the remaining namespaces.
Upgrading
Section titled “Upgrading”# Upgrade to the latest published versionalter self-update
# Pin a specific version, or preview the command without running italter self-update --to 0.3.0alter self-update --to nextalter self-update --dry-run--to accepts strict SemVer (including prerelease/build metadata) or the npm
tags latest and next; other values fail locally with usage code 2.
Legacy self-update --version is rewritten to --to with a deprecation
warning, in every form it was typed: --version <value>, --version=<value>,
and a bare --version. The bare form has no version to pin, so it reports
option '--to <version>' argument missing and exits 2. Run
alter --version for the installed CLI version, before any command name.
self-update runs npm install -g @alter-ai/cli@<version> and smoke-tests
the resolved binary. If the active binary is not under npm’s global prefix,
the command refuses with usage code 2; update Homebrew, apt, or other
package-manager installations through that manager. A failed npm install
forwards npm’s own exit code.
After a successful interactive command, the CLI may print a newer-version
notice to stderr. It checks npm at most once per 24 hours, caches under the
XDG cache directory, skips piped stdout, --version, and self-update, and
silently ignores registry/cache failures. Use one of the notifier opt-out
variables in the environment table when deterministic interactive stderr is
required.