Skip to content

Exit codes

Code Meaning
0 Did what you asked.
1 Ran and failed — a rejected request, an unreachable host, a workflow file that would not parse.
2 Usage error: clap rejected the invocation before anything ran. Unknown subcommand, missing required argument, a value outside an enum.

The 1/2 split is the point of the table. A wrapper script keys on 2 to tell “you typed it wrong” from “it tried and failed”, so it is a contract rather than whatever the argument parser happened to return, and it is pinned by a test that drives the built binary.

neo pr wait adds five codes above that range, one per terminal state of a pull request, because the whole reason to have it is that a shell loop around neo pr view had to grep prose to tell these apart:

Code Meaning
3 A check on the PR’s head failed. The failing contexts are listed on stdout. --for checks only.
4 The PR was closed without merging.
5 Every check finished and the merge gate still refuses, so the PR will not merge without someone acting — a stale head under strict_up_to_date, a missing approval, an unresolved thread, a draft. The server’s own reasons are printed verbatim. --for merged only.
6 --timeout elapsed with no terminal state.
7 Every check on the head finished and every one was skipped, so nothing actually ran. Not a pass, and nothing failed. --for checks only.

They start at 3 rather than reusing 1 on purpose: a wait that could not reach the forge is not a red check, and a wrapper keying on 3 must not fire on an unplugged network cable. neo actions watch predates this and still collapses a failed run onto 1.

Two things worth knowing about the split between 3 and 5:

  • 3 is --for checks only. “Is anything red?” is a question the client can answer completely — it folds both check sources the merge gate judges, workflow runs and externally-posted commit statuses. “Is this red check the reason the merge is blocked?” is not: which contexts a branch REQUIRES is protection config the client cannot read, so a red optional lint must never be reported as the blocker on a PR the gate is refusing for a missing approval. Under --for merged a gate refusal is always 5, carrying the gate’s own reasons, with any red contexts shown underneath as detail.
  • 5 has one documented blind spot. The gate scores a required context that has reported nothing as pending, and a context with no row in either source is invisible to the client. So a gate blocked solely on a required context that has never posted looks, from outside, like a gate blocked on something human, and is reported as 5 early. Every other self-resolving blocker is visible: a pending external status is a real row and is counted. 5 is confirmed by a second independent read before it is reported, which guards a torn read across the three GETs one poll makes — it does not close this blind spot, and does not claim to.

Two more commands add a rule of their own:

  • neo doctor exits non-zero only when something breaks the core forge workflow. Missing docker, nix or jj is a warning, because only neo run needs them — someone who files issues and opens pull requests must never see a red report for tools they do not use.
  • neo repo create is idempotent: a repository that already exists is reported as such and exits 0.