Skip to content

Exit codes are an API — distinguish retryable from not

scope: generic · severity: law · confidence: proven · subsystem: method

A tool that returns 1 for everything forces its caller to parse English. In an agentic loop that means retrying things that will never succeed, and giving up on things that would have worked on the second try.

The convention porthole uses, and any tool in it must follow:

code meaning caller should
0 success continue
1 the thing under test failed report it — this is a result, not an error
64 usage error fix the invocation
69 the tool could not run at all not a finding — the check did not happen
75 could not get the device lock retry — someone else has it
76 device in the wrong state do not retry — something must move it
124 killed at the hold ceiling a wedge; investigate, do not just rerun

The 75/76 split is the one that earns its keep, and it exists because the alternative cost two agents ten minutes each. Waiting fixes a 75. Waiting never fixes a 76.

Keep 1 for “the measurement says no”, and 69 for “no measurement happened”. That split is not decoration: porthole aports lint printed “lint found problems” for a subcommand pmbootstrap 3.11.1 had removed, and porthole channel <name> --yes printed “pmbootstrap refused the channel change” for a config key that no longer exists. Both were exit 1 – a broken tool wearing the costume of a finding about the user’s work. An agent that cannot tell the two apart reports broken tools as findings, and the porter fixes a package that was never wrong.

So: if the tool could not run – not installed, subcommand gone, no workspace and no host toolchain – exit 69 and say what is missing. Never render an unavailable check as a negative answer.

Related: the-lock-says-who-not-what.