Skip to content

CLI exit codes and failure output

The exit status of airdress and its failure report are a stable contract: scripts, the deploy-functions GitHub Action, the editor extension and MCP clients read them. A code is never renumbered or reused for another meaning; a new kind of failure gets a new number.

CodeNameMeaningTypical causes
0okThe command did what it was asked.Includes airdress update --check when an update is available.
1internalUnexpected: a bug, an answer the CLI could not read, a local file it could not read or write, a 5xx from the server itself.500, a malformed body, a disk error.
2usageThe command line is wrong.An unknown subcommand or flag, a missing argument, a missing signing key (signer_unavailable).
3refusedYour airdress or the hub refused the request, or a person declined at a prompt.403, 404, 422, 429, a dry run’s refusal, capability_not_granted, confirmation_declined.
4authAuthentication is needed or has expired.No profile, no sign-in, a refresh the hub refused, 401, machine_authorization_expired.
5networkThe other end could not be reached or did not answer in time.Connection refused, DNS failure, timeout, 408, 502, 503, 504.
6conflictThe request lost a race with another writer.409, 412, source_base_stale, epoch_conflict, root_key_mismatch.
7confirmation requiredThe command needs a yes and could not ask: stdin is not a terminal, or --output json is set. Pass -y / --yes.A destructive command run from a script.

A refusal is classified by its code first (a source_base_stale is 6 whatever its HTTP status), then by its HTTP status.

Text mode (the default) writes to stderr:

Error: <what failed: its cause>
hint: <the one useful next step, when there is one>

--output json writes exactly one JSON object to stderr, on one line, and nothing else for the failure:

{"code":"source_base_stale","message":"stale base: …","hint":"re-base the tree …","status":409}
FieldAlwaysMeaning
codeyesA stable snake_case identifier: your airdress’s own code verbatim (epoch_conflict, publication_blocked), or one of the CLI’s (internal, usage, auth_required, not_signed_in, sign_in_expired, profile_not_found, unreachable, timeout, confirmation_required, http_<status>). A script decides on code and the exit status, never on message.
messageyesFor a person. May change between releases.
hintnoWhat to do about it.
statusnoThe HTTP status that was answered, when there was one.
anything elsenoEvery other field of the refusal (reason, locations, denials, basedOn, current, …), carried through unchanged.

A command that prints a structured result on stdout (a deploy’s reports, a refusal of fn publish) still does; the failure object on stderr is in addition to it.

Terminal window
airdress fn deploy ./hello --yes --output json >deploy.json 2>err.json
case $? in
0) echo "deployed" ;;
4) airdress auth login ;;
6) echo "someone else deployed first: $(jq -r .current err.json)" ;;
*) jq -r '.code + ": " + .message' err.json ;;
esac

Exits 0 whether or not an update is available. --check is a question, and answering it is success; the answer is in the output (Update available: a -> b, or "update_available": true under --output json). A non-zero exit means the check itself failed, which is the case a script most needs to notice.

  • airdress shell ends with the remote program’s own exit status, passed through unchanged, as ssh does. A failure of the shell client itself (before the program ran, or a lost session) uses the codes above, but a remote program may exit with any of 1 to 255, including those numbers.
  • A panic in a serving mode (shell host, mcp serve, and airdress-agent device serve) exits 101, Rust’s own code for a panic.
  • Help and --version exit 0; asking for help is not a failure.

Every command that changes something it cannot undo asks first, through one prompt, and every one of them takes -y / --yes: device revoke, fn deploy, fn signers add|remove, home disconnect, plugins install|uninstall|deauthorize, shell close, shell device forget.

  • With -y / --yes it does not ask.
  • Under --output json, or when stdin is not a terminal, it never asks: it exits 7 with confirmation_required, a hint naming --yes, and "flag": "--yes" in the JSON object.
  • Otherwise it asks … [y/N] on stderr. The default is No: only y or yes (any case) confirms; anything else, an empty line or end of input, is a decline, exit 3 with confirmation_declined.

Two questions only a person can answer, so no flag answers them and they exit 7 without a terminal: shell host trust (trusting a device by its fingerprint) and a shell host’s first-use key (“Does it match?”), including the join question asked inside airdress shell.