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_ERRORshould be rare -
Emitting
ARG_ERROR (2)after a side effect. Code2carries a hard guarantee of zero side effects. If any write occurred before the error, emitPARTIAL_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: readerror.code, resolve it, reissue. An agent that gives up on auto-refreshable token expiry wastes the task -
Confusing
PERMISSION_DENIED (7)andAUTH_REQUIRED (8).PERMISSION_DENIEDmeans the credentials are valid but insufficient — retrying with the same credentials will never succeed.AUTH_REQUIREDmeans 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 exitsINCOMPLETE (14)withdata.continue_command.TIMEOUTmeans 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, declareretryable: true, side_effects: "none"inExitCodeEntry. Declareretryable: falsewhen partial writes may have occurred, even on a command declaredidempotent: theExitCodeEntryinvariant requires it whenside_effectsis"partial" -
Declaring every read-only
TIMEOUT (10)retryable.side_effects: "none"permitsretryable: truebut 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: declareretryable: false, side_effects: "none", and the error carriesfix_requirednaming 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
retryableandside_effectsvalues in this table describe the default agent behavior when no per-commandExitCodeEntryis available. When a command's manifest or--schemaoutput includes anExitCodeEntryfor 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.TIMEOUTwith partial side effects)- The hard invariants that commands may not relax:
ARG_ERROR (2)is alwaysside_effects: none;PARTIAL_FAILURE (3)is alwaysretryable: false;SUCCESS (0)is the only code withside_effects: complete ARG_ERROR (2)requires a hard phase boundary between validation and execution. No side effect may begin before this code can be emittedAUTH_REQUIRED (8)intentionally does not distinguish expired from invalid at the exit code level. The distinction is inerror.codein the JSON payload — a more controlled channel. Seeresponse-envelope.jsonErrorDetail.codevalues:TOKEN_EXPIRED,TOKEN_INVALID,TOKEN_MISSINGINCOMPLETE (14)requiresdata.continue_commandanddata.progressin the response (REQ-F-082). Itsretryablefollows the command's declared entry:truewithside_effects: "none"for read-only work, whose identical re-run continues;falsewithside_effects: "partial"for mutating work, which the agent continues throughcontinue_commandrather than retriesREDIRECTED (13)requires theerror.redirectfield in the response. Seeresponse-envelope.jsonRedirectdefinition