Exit Statuses as a Shell API

When a command-line program finishes, it hands back an integer between 0 and 255 to the calling shell. That value, inspectable with echo $?, is one of the primary channels through which scripts and shells interpret what happened inside the process they launched. The conventions around those numbers are loose but widely understood: 0 means success, 1 means a generic failure, and 2 often indicates incorrect usage.

The ambiguity begins when a program wants to signal something more specific than "it broke." For example, a process killed by a fatal signal returns 128 + n, where n is the signal number. A SIGINT (signal 2, i.e., Ctrl+C) therefore surfaces as an exit status of 130:

$ curl -n https://api.heroku.com/apps
^C

$ echo $?
130

There is no official registry for custom exit codes, but two sources offer rough guidance. The Advanced Bash-scripting Guide catalogs a set of reserved codes, and the kernel header sysexits.h attempts a standardization for C and C++ programs:

#define EX_OK		0	/* successful termination */

#define EX__BASE	64	/* base value for error messages */

#define EX_USAGE	64	/* command line usage error */
#define EX_DATAERR	65	/* data format error */
#define EX_NOINPUT	66	/* cannot open input */
#define EX_NOUSER	67	/* addressee unknown */
#define EX_NOHOST	68	/* host name unknown */
#define EX_UNAVAILABLE	69	/* service unavailable */
#define EX_SOFTWARE	70	/* internal software error */
#define EX_OSERR	71	/* system error (e.g., can't fork) */
#define EX_OSFILE	72	/* critical OS file missing */
#define EX_CANTCREAT	73	/* can't create (user) output file */
#define EX_IOERR	74	/* input/output error */
#define EX_TEMPFAIL	75	/* temp failure; user is invited to retry */
#define EX_PROTOCOL	76	/* remote error in protocol */
#define EX_NOPERM	77	/* permission denied */
#define EX_CONFIG	78	/* configuration error */

#define EX__MAX	78	/* maximum listed value */

Choosing a Code for a 2FA Challenge

This question came up while building hk, a Heroku CLI client, during a discussion about two-factor authentication. The tool's design philosophy is that most commands should be non-interactive by default, with a few exceptions like hk login. That makes it safe to compose hk commands into shell scripts without worrying that a prompt will unexpectedly block automation.

With 2FA added to the Heroku API, a two-factor code could now be required on any given call, depending on the endpoint and the sensitivity of the operation. An interactive client can simply prompt the user mid-call, but a non-interactive tool has different constraints. The chosen behavior in hk is to fail with a well-known exit status that explicitly marks the failure as a 2FA problem. That way, a script can detect the condition and take a reasonable follow-up action, such as notifying an operator that a fresh code is needed.

The harder part was picking the number. The Advanced Bash-scripting Guide makes a practical proposal: restrict user-defined codes to the range 64–113, leaving 0 for success. That range stays clear of the reserved codes and the sysexits.h assignments (which generally land below 79). Starting at 79, after skipping the sysexits.h entries, yields a sensible lower bound for application-specific statuses. That is the convention adopted in hk's implementation:

$ hk env -a paranoid
error: A second authentication factor or pre-authorization is required
for this request. Your account has either two-factor or a Yubikey
registered. Authorize with `hk authorize`.

$ echo $?
79

For a tool that wants to distinguish failure classes reliably, staying within a bounded, documented range beats inventing arbitrary numbers. It keeps the exit-status contract readable and gives script authors a dependable way to branch on the cause rather than just the fact of a failure.