mirrorball
DocsReference

Exit codes

Every exit code mirrorball can produce, when it occurs, and which error code maps onto it.

mirrorball's exit codes are part of its interface. They are small in number and each one names a class of failure, so a script can branch on "the port was taken" without parsing English. The explanation always goes to stderr; the code is what you test.

The codes

CodeNameWhen it occurs
0OKThe command did what it was asked. ls, stop and logs on success; a --background start that came up ready or degraded.
1GENERICA failure with no more specific class: an internal error, or a session id that matched nothing. Also what bunli itself exits with when an option fails validation.
2USAGEBad arguments, or a config file mirrorball cannot use. Nothing was attempted.
3SSHssh could not connect, could not authenticate, or could not be found.
4PORT_CONFLICTA local port is already bound, or is privileged and mirrorball is not.
5REMOTE_REFUSEDThe tunnel was fine and the remote service was not there.
130SIGINTYou stopped it. The normal way a foreground session ends.

Anything outside this table came from the runtime, not from mirrorball.

Error codes

Every failure inside mirrorball is a MirbError carrying a MirbErrorCode. That code is what decides the exit status, and it is also what appears in a forward.error event.

MirbErrorCodeExitMeaning
USAGE2Bad arguments: no host, no ports, an unparseable port spec, a duplicate local port, a --bind that would publish the forward without --expose.
CONFIG2config.toml exists but is malformed, or names a setting mirrorball does not have.
PORT_IN_USE4A local port is already bound. Raised by mirrorball's own preflight, and by ssh's bind failure when something takes the port in between.
PORT_PRIVILEGED4A local port below 1024 without the privileges to bind it.
SSH_AUTH3Permission denied, too many authentication failures, or host key verification failed.
SSH_CONNECT3DNS failure, connection refused, timeout, a local port that never started accepting — and the fallback for any ssh failure mirrorball cannot classify.
NO_SSH3No ssh binary on PATH, or $MIRB_SSH / --ssh-path pointing at something that is not executable.
REMOTE_REFUSED5ssh reported a channel-open failure and exited. The tunnel worked; nothing was listening at the far end.
SESSION_NOT_FOUND1No background session matches the id, name or host given — or an id prefix that matches several, which mirrorball refuses to guess at.
INTERNAL1A bug, or an unexpected runtime failure. Includes a --background supervisor that never reported a working tunnel.

SSH_CONNECT being the fallback is deliberate: an ssh that died mid-session for unclear reasons is far more often a network problem than anything else, and SSH_CONNECT is one of the few codes mirrorball will retry on.

Notes on particular cases

A degraded session is not a failure. A tunnel that came up with one forward refused exits like any other: 0 from --background, 130 when you Ctrl-C the foreground. mirrorball told you which forward is unusable — in forwards[].status, in a forward.error event, and in amber on the display — and throwing away the forwards that work would be a worse answer than reporting the ones that do not. Exit code 5 is for something else: ssh itself giving up with a channel-open failure as its last word.

A foreground mirb up does not exit 0. It runs until you stop it (130) or until it gives up (a class code). There is no third ending. Scripts that want a success code should use --background, which exits 0 once the tunnel is proven, or watch for session.ready on the event stream.

SIGTERM also exits 130. mirrorball treats any requested stop as the same event, whether it arrived as Ctrl-C or as a signal from a process manager. This is deliberately not the conventional 143: 130 here means "someone asked mirrorball to stop, and it did", which is exactly what happened.

--background propagates the supervisor's code. When the detached supervisor dies before reporting a working tunnel, the parent reads the failure out of the supervisor's log, prints it, removes the half-written record, and exits with the child's code — so mirb example.test 3000 -b against an unreachable host still exits 3, not 1. A child that somehow exited 0 without coming up is reported as 1.

Ambiguity is an error, not a guess. mirb stop k3 matching two sessions exits 1 and lists both. The command on the other end of that decision kills processes.

bunli owns a few exits of its own. --help and --version exit 0. An option that fails validation exits 1 with a structured error on stderr, before mirrorball's handler ever runs. Unrecognised flags are ignored rather than rejected.

Checking them

mirb example.test 45241:3000 --background --json > session.json
case $? in
  0) echo "up" ;;
  4) echo "local port taken" ;;
  3) echo "cannot reach the host" ;;
  *) echo "see stderr" ;;
esac

The codes are stable. They are defined once, in EXIT in core/types.ts, and the mapping from error code to exit code lives in MirbError.exitCode in core/errors.ts — one switch, covered by tests, so stop and up cannot disagree about what a busy port is worth.

See also

  • JSON output — the envelope, the event stream, and where errors are written
  • CLI reference — which command can produce which failure
  • Configuration — a malformed config.toml is exit 2

Last updated on

On this page