Skip to content

Schema: ExitCode

File: exit-code.json

Used by: REQ-F-001 · REQ-C-001 · REQ-C-013 · REQ-O-041 · REQ-F-082


Purpose

Most CLI tools use only 0 (success) and 1 (failure), forcing agents to parse error messages to understand what went wrong and whether retrying is safe. ExitCode replaces this with a fixed table where every code carries two machine-readable guarantees: whether the operation is retryable and how far side effects progressed. An agent can decide its next action from the exit code alone.

retryable answers exactly one question: may the agent re-run the identical invocation, unchanged, with a chance of success? yes additionally guarantees no side effects occurred. Conditions the caller must correct first are marked after fix and surface in the envelope as retryable: false with fix_required (or error.redirect) present.

Codes are sequential and grouped by category. The group boundaries are visible in this table — they are not encoded in the number itself.


Values

Success

Code Constant Retryable Side effects Agent action
0 SUCCESS — complete Done

Execution — operation ran, something went wrong internally

Code Constant Retryable Side effects Agent action
1 GENERAL_ERROR depends unknown Inspect error.detail — last resort, use a specific code whenever one exists
3 PARTIAL_FAILURE no partial Inspect state before retrying — some writes occurred; a command declared idempotent: true reruns unchanged once instead

Input — caller's fault; fix the input, then reissue

Code Constant Retryable Side effects Agent action
2 ARG_ERROR after fix* none Fix the input, then reissue — zero side effects guaranteed
4 PRECONDITION after fix* none Resolve the precondition, then reissue

Resource — about the addressed entity

Code Constant Retryable Side effects Agent action
5 NOT_FOUND no none Stop, or create the resource first
6 CONFLICT no none Resolve the conflict (delete, rename, or bump version)

Auth — identity and access

Code Constant Retryable Side effects Agent action
7 PERMISSION_DENIED no none Stop — valid credentials, wrong permissions; escalate or change approach
8 AUTH_REQUIRED after fix* none Read error.code: TOKEN_EXPIRED → auto-refresh, then reissue; TOKEN_MISSING / TOKEN_INVALID → acquire credentials
9 PAYMENT_REQUIRED after fix* none Attempt x402 payment if agent has payment permission, then reissue

Infrastructure — external systems

Code Constant Retryable Side effects Agent action
10 TIMEOUT depends partial Inspect state before retrying; retry directly only when the command's TIMEOUT entry declares retryable: true (which requires side_effects: "none"), reissue with a larger --timeout when error.fix_required names one, or rerun once unchanged when it declares idempotent: true
11 RATE_LIMITED yes none Retry after error.retry_after_ms milliseconds
12 UNAVAILABLE yes none Service temporarily down — apply exponential back-off, retry

Routing — API surface changed

Code Constant Retryable Side effects Agent action
13 REDIRECTED after fix* none Use error.redirect.command verbatim; if error.redirect.permanent is true, memorize — never call the old form again

Continuation — work not finished, not lost

Code Constant Retryable Side effects Agent action
14 INCOMPLETE per entry none or partial Run data.continue_command verbatim; stop and run data.cancel_command when data.progress.done stops growing across continuations (REQ-F-082)

* after fix: the identical invocation fails until the caller corrects the stated condition, then reissues. Declared as retryable: false in ExitCodeEntry; the envelope carries fix_required (or error.redirect) with the correction.

Reserved ranges: - 15–63 framework extensions - 64–78 POSIX sysexits compatibility (optional mapping) - 79–125 command-specific (declare per REQ-C-001) - 126–255 shell-reserved: commands MUST NOT emit these; framework signal handlers alone emit 128 + N (130 on SIGINT per REQ-F-069, 143 on SIGTERM per REQ-F-013), declared in every command's exit_codes with retryable: false and side_effects: "partial"


Examples

Valid

0
3
11

Valid — TIMEOUT from a read-only command whose run length depends on its input

10

The command declares the code as retryable: false with side_effects: "none": nothing was written, but the identical re-run times out again, so the error carries fix_required naming a larger --timeout:

{ "name": "TIMEOUT", "description": "Scan exceeded --timeout; the same input needs a larger --timeout", "retryable": false, "side_effects": "none" }

Schema-valid but wrong by convention

1

Violation: GENERAL_ERROR is a last resort. If the condition matches any specific code (3–14), use that code instead.

Invalid — framework extension range

15

Violation: code 15 is in the framework extensions range (15–63), reserved for future use. Commands must not emit it.

Invalid — shell-reserved range

130

Violation: 128 + SIGINT is shell-reserved. A command must not choose it; only the framework's SIGINT handler emits it (REQ-F-069).


Common mistakes

  • Using GENERAL_ERROR (1) as the default. Every condition that maps to a specific code must use that code. GENERAL_ERROR should be rare

  • Emitting ARG_ERROR (2) after a side effect. Code 2 carries a hard guarantee of zero side effects. If any write occurred before the error, emit PARTIAL_FAILURE (3) instead. The framework phase boundary (validate → execute) makes this automatic — do not bypass it

  • Treating AUTH_REQUIRED (8) as terminal. The identical call fails until credentials are fixed (retryable: false), but the condition is correctable: read error.code, resolve it, reissue. An agent that gives up on auto-refreshable token expiry wastes the task

  • Confusing PERMISSION_DENIED (7) and AUTH_REQUIRED (8). PERMISSION_DENIED means the credentials are valid but insufficient — retrying with the same credentials will never succeed. AUTH_REQUIRED means the credentials themselves are the problem

  • Emitting TIMEOUT (10) for work that goes on. A command that hands its work to the background or saves it at a checkpoint when the caller's budget runs out has not timed out: it exits INCOMPLETE (14) with data.continue_command. TIMEOUT means the work stopped and its progress since the last checkpoint is gone

  • Treating all TIMEOUT (10) exits as non-retryable. For operations that time out before any write on a transient stall the identical re-run can clear, declare retryable: true, side_effects: "none" in ExitCodeEntry. Declare retryable: false when partial writes may have occurred, even on a command declared idempotent: the ExitCodeEntry invariant requires it when side_effects is "partial"

  • Declaring every read-only TIMEOUT (10) retryable. side_effects: "none" permits retryable: true but does not imply it. A read-only command whose run length depends on its input (a scan of a large tree, a query over a large range) times out again on the identical re-run: declare retryable: false, side_effects: "none", and the error carries fix_required naming a larger --timeout


Agent interpretation

Rules for agents consuming exit codes at runtime. Apply these when the response is ambiguous, contradictory, or outside the known table.

Unknown code received - Code in 15–63 — framework extension; treat as GENERAL_ERROR (1) behavior: inspect error.detail, do not assume retryability - Code in 64–78 — POSIX sysexit; look up the POSIX meaning; treat as non-retryable unless the meaning clearly indicates a transient condition - Code in 79–125 — command-specific; consult that command's exit_codes declaration from the manifest before acting - Code 126 or 127 — the binary could not be executed or was not found; the command never ran; fix the environment, then reissue - Code in 129–159 (128 + signal) — the process was killed or cancelled mid-run (outer timeout, OOM, SIGINT, SIGTERM); side effects may be partial; inspect state before any retry, unless the command's manifest entry declares idempotent: true and its declared entry for the code has side_effects: "partial", which makes one unchanged rerun the recovery - Code outside 0–255 — treat as GENERAL_ERROR (1); log for investigation

Contradictory signals - Envelope and process exit code disagree in either direction — treat the call as failed; the side that reports failure wins - ok: false but process exit code is 0 — a pipeline or wrapper masked the code (§56); use meta.exit_code for classification - Exit code says retryable but error.retryable: false — trust error.retryable; it is the more specific signal - Any code with error.code: "DELEGATED_EXIT" — a passthrough command (REQ-C-031) passed through its delegated tool's own code, and this table does not apply; a delegated 2 carries no zero-side-effect guarantee. The envelope is the last line of stderr

Retry budget - Codes marked retryable do not imply infinite retries — apply a retry budget (e.g. 3 attempts) before escalating - Codes marked after fix — apply the fix stated in error.fix_required (or error.redirect), reissue once; if no fix is stated, treat as terminal - RATE_LIMITED (11) without error.retry_after_ms — default to 60 seconds before retrying - UNAVAILABLE (12) — use exponential back-off starting at 1s, cap at 5 minutes - INCOMPLETE (14) — not a retry: run data.continue_command, which is either a wait on the running job or the original invocation resuming from its checkpoint. Compare data.progress.done between continuations; when it has not grown over three of them, run data.cancel_command and escalate

Side effects under uncertainty - Code is GENERAL_ERROR (1) (side effects unknown) — treat as side_effects: "partial"; inspect state before retrying - Code is TIMEOUT (10) — partial side effects are possible; do not assume the operation is idempotent unless its manifest entry declares idempotent: true, which makes rerunning the identical command once the recovery


Coding agent notes

Reading exit-code.json vendor extensions - x-enum-varnames: array of constant names parallel to enum values — use to generate the enum type (e.g. ExitCode.SUCCESS = 0) - x-enum-descriptions: array of descriptions parallel to enum values — use as docstrings on each enum member - x-groups: map of group name → list of codes — use to generate helper predicates (e.g. ExitCode.is_auth(code)) - x-code-ranges: documentation of reserved ranges — validate against these ranges at registration

Type representation - Generate ExitCode as an enum type, not integer constants — the type system must reject bare literals at the call site, not just at runtime - The enum must be serializable to its integer value for JSON output and process exit; do not serialize the name string

Validation to generate - A registration-time check that rejects any ExitCode value outside the declared enum members - A runtime check in dev/test mode that intercepts process.exit / sys.exit and asserts the code matches the command's declared set

Tests to generate - For each code: a test that emits the code and asserts the correct integer reaches the process exit - A negative test: emitting a literal integer (e.g. 5) at a call site that only accepts ExitCode must fail at compile/type-check time, not at runtime - A test that ARG_ERROR (2) is only emitted before any mock side-effect function is called

Anti-patterns - Do not generate if code == 7 comparisons — always compare against ExitCode.PERMISSION_DENIED - Do not generate a fallback except: exit(1) — map every known exception to a specific ExitCode - Do not expose a method that accepts int where ExitCode is expected


Implementation notes

  • Represent as a named constant / enum — never a bare integer at call sites. The framework must reject literal integers at command registration
  • retryable and side_effects values in this table describe the default agent behavior when no per-command ExitCodeEntry is available. When a command's manifest or --schema output includes an ExitCodeEntry for the received code, that entry takes precedence — the command may declare a code as non-retryable even if this table marks it retryable (e.g. TIMEOUT with partial side effects)
  • The hard invariants that commands may not relax: ARG_ERROR (2) is always side_effects: none; PARTIAL_FAILURE (3) is always retryable: false; SUCCESS (0) is the only code with side_effects: complete
  • ARG_ERROR (2) requires a hard phase boundary between validation and execution. No side effect may begin before this code can be emitted
  • AUTH_REQUIRED (8) intentionally does not distinguish expired from invalid at the exit code level. The distinction is in error.code in the JSON payload — a more controlled channel. See response-envelope.json ErrorDetail.code values: TOKEN_EXPIRED, TOKEN_INVALID, TOKEN_MISSING
  • INCOMPLETE (14) requires data.continue_command and data.progress in the response (REQ-F-082). Its retryable follows the command's declared entry: true with side_effects: "none" for read-only work, whose identical re-run continues; false with side_effects: "partial" for mutating work, which the agent continues through continue_command rather than retries
  • REDIRECTED (13) requires the error.redirect field in the response. See response-envelope.json Redirect definition