Status: Accepted (2026-04-14)
Each bin originally exited 0 on success and 1 on any other outcome.
Operators running these in CI couldn’t distinguish “a validation rule
failed” (an expected outcome; the pipeline should fail the PR check) from
“the CLI was invoked with a bad flag” (a workflow-author bug; the pipeline
should fail the workflow author’s attention, not the PR author’s).
A single named enum, exported as EXIT_CODES, consumed by every bin:
| Name | Value | Meaning |
|---|---|---|
OK |
0 | Success |
VALIDATION |
1 | One or more validation rules failed (expected failure mode) |
ENV |
2 | Misconfigured environment — missing file, bad git repo, unreadable facts |
USAGE |
64 | Bad CLI invocation — unknown flag, missing required positional |
64 is chosen deliberately: it matches BSD sysexits.h EX_USAGE. Pipeline
authors can then write:
- run: npx dotbabel-validate-specs
- if: failure()
run: |
case $? in
1) echo "validation failed — review the PR"; exit 1 ;;
2) echo "environment issue — check workflow setup"; exit 2 ;;
64) echo "bad CLI invocation — the workflow needs editing"; exit 64 ;;
esac
ENV vs USAGE vs VALIDATION route to
different humans.sysexits.h alignment. Future codes (e.g. EX_NOPERM=77 for “hook
blocked”) have a standard vocabulary to pick from.guard-destructive-git.sh hook’s exit 2 stays — it’s the
Claude Code PreToolUse protocol (block the tool call), not the harness
validator ENV code. Documented explicitly in the hook header comment.64 is recognized,
invented codes aren’t.rustc’s exit codes. Rejected — not a convention operators
expect from a CLI.{OK, VALIDATION, ENV,
USAGE}. Most likely a “blocked by policy” (hook) case, which would
adopt 77.