Skip to main content
Every ish command exits with a semantic code. Branch on the code in CI and agent loops, and parse the JSON envelope (in --json mode) for the structured cause. The code answers “what class of failure”, the envelope answers “what exactly, and what to do next”.

Exit codes

CodeMeaning
0Success.
1General error (including billing 403: out of credits or usage limit).
2Usage or validation error (bad flags, missing arguments).
3Authentication failure (no or invalid token).
4Not found (the workspace, study, or other entity does not exist).
5Transient error, safe to retry.
In --json mode, errors carry a structured envelope: error, error_code, retryable, and often suggestions, error_kind, and an example invocation that fixes the call. The map is computed by exitCodeFromError in command-helpers.ts. A successful command exits 0; -h, --help and -V, --version also exit 0.

How a failure resolves to a code

The resolver checks the most specific signal first, so a tagged error always beats a status guess and a status guess always beats a message-regex sniff.
1

Billing walls (exit 1)

A usage_limit_reached or insufficient_credits error is checked before the status map. These arrive as HTTP 403 but are not auth failures, so they exit 1, not 3. An agent branching “exit 3 means re-login” would otherwise loop forever on a quota cap.
2

HTTP status (exit 3, 4, 2, 5)

For other API errors: 401 and 403 exit 3, 404 exits 4, 400 and 422 exit 2. A retryable status (408, 429, or any 5xx) exits 5.
3

Tagged error code (exit 2, 3, 4)

A CLI-thrown error carrying error_code is mapped directly: usage_error exits 2, auth_failed exits 3, not_found exits 4.
4

Retryability and error kind (exit 5, 2)

wait_timeout and any error pre-declaring retryable: true exit 5. A TunnelInactive or BotAuthError kind exits 5 (the cause is fixable, then retry); ConfirmationRequired or BotShapeError exit 2.
5

Network cause (exit 5)

DNS and connection failures (ENOTFOUND, ECONNREFUSED, ECONNRESET, ETIMEDOUT, EAI_AGAIN) are transient and exit 5.
6

Fallback (exit 1)

Anything unclassified exits 1.
Out of credits exits 1, not 3. A 402 or a billing 403 is a quota cap, not an auth problem. Re-running ish login will not fix it. Read error_code to tell insufficient_credits and usage_limit_reached apart from a real auth failure.

The error envelope

In --json mode (or when stdout is piped, which auto-selects JSON), a failing command writes a single-line JSON object to stderr and the exit code to the process. Human mode prints Error: <message> followed by indented suggestion lines instead. See global flags for when JSON is auto-selected.

Always present

error
string
The human-readable message. For API errors, server-internal entity names are remapped to the user-facing vocabulary before printing.
error_code
string
The stable machine code to branch on. Prefer this over the message text, which can change. See the error codes table.
retryable
boolean
Whether the same call may succeed if retried unchanged. true corresponds to exit 5.

Often present

These fields appear only when the failure carries them, so a consumer must treat each as optional.
status
number
The HTTP status, present only on errors that came back from the API.
suggestions
string[]
Recovery hints, for example “pass --study or run ish study use”. Merged from the server response, the error instance, and the CLI’s own code-to-hint mapping.
errors
array
Field-level validation detail from a 422 response. Each entry carries loc, msg, type, and, where the field is an enum, allowed_values.
error_kind
string
A structured kind for failures that have one, such as TunnelInactive, ConfirmationRequired, BotAuthError, or BotShapeError.
example
string
A corrected invocation that fixes the call, for example the same command with --yes appended when a destructive action needs confirmation in --json mode.
hint
string
A one-line hint on a client-side validation error.
valid_options
string[]
The accepted values when a validation error rejected an enum-shaped input.
available_values
string[]
The set a filter argument could have matched (for example the frames a --frame value could resolve to).
progress
object
How far a wait got before it timed out, on a wait_timeout error.
seeded_but_not_dispatched_ids
string[]
Participants seeded before a dispatch failed. Resume with these rather than re-seeding, which would create duplicates. A matching seeded_but_not_dispatched_aliases rides alongside.
report
string
A hint to report the failure, present only on genuine faults (not on usage errors).

Billing fields

On a usage_limit_reached error, the envelope also carries tier, limit, current, max, and upgrade_url. See run vs ask for what draws credits.

Error codes

The error_code field is the stable contract. API errors map from the HTTP status; CLI-thrown errors set the code directly.
error_codeSourceTypical exit
auth_failedAPI 4013
forbiddenAPI 4033
insufficient_creditsAPI 402, billing 4031
usage_limit_reachedbilling 4031
not_foundAPI 404, CLI not_found4
validation_errorAPI 422, CLI validation2
usage_errorCLI usage2
timeoutAPI 4085
rate_limitedAPI 4295
server_errorAPI 5xx5
network_errorDNS / connection failure5
wait_timeoutish study run wait timer5
request_failedother API error1
client_erroruntagged CLI error1
unknown_errornon-Error throw1

Example

A study id that does not exist returns exit 4 with this envelope on stderr:
{
  "error": "Study not found: s-xxxx",
  "error_code": "not_found",
  "status": 404,
  "retryable": false
}
Branch on the code in a script:
ish study results --study s-xxxx --json
case $? in
  0) echo "ok" ;;
  3) echo "re-authenticate" ;;       # auth_failed / forbidden
  4) echo "wrong id, do not retry" ;; # not_found
  5) echo "transient, retry" ;;       # timeout / rate_limited / network
  *) echo "failed" ;;
esac
Branch on the exit code for control flow and read error_code for the precise cause. Both are stable; the error message text is not.