Wheelhouse docs

Exit codes#

Every program here reports its result as an exit status. wheelhouse-agent plan follows diff -q: exit 2 means the router has drifted, so a shell test needs no text parsing.

wheelhouse-agent, the daemon#

CodeWhen
0version or --version. Also a clean shutdown after SIGTERM or SIGINT.
2The command line or the configuration is wrong, and the agent never started.
1The agent could not open something it needs, or the listener died.

Exit 2, before anything is opened:

  • an unexpected non-flag argument, or an unknown subcommand — both print the usage text;
  • a flag the flag package cannot parse;
  • neither --demo nor both --api-url and an API key;
  • a secret file that cannot be read, or one that is group- or world-readable;
  • single sign-on misconfigured — see the validation list;
  • --reconcile-mode that is not stage or commit;
  • --tls-cert without --tls-key, or the reverse.

Exit 1, after startup began:

  • the data directory cannot be created;
  • another agent already owns that data directory — another agent is running against this data dir;
  • the state file was written by a newer agent (the state file is newer than this agent);
  • catalog.json will not parse — app catalog is invalid;
  • the first admin account could not be created;
  • --tls-self-signed could not generate a certificate;
  • --fleet-config will not load, or is not mode 0600;
  • the listener stopped with anything other than a clean shutdown.

A handler that panics does not exit the process: it is caught, logged with its stack, and answered as a 500.

wheelhouse-agent plan#

CodeMeans
0The router already matches the file.
2It does not. There are operations to apply.
1The file will not parse, holds an empty value, contradicts the router, or the router did not answer.
bash
if ! wheelhouse-agent plan --file /config/wheelhouse/agent.yaml >/dev/null; then
  echo "drift"
fi

2 is not a failure, it is an answer. A script that treats every non-zero status as an error reports drift as a crash.

wheelhouse-agent apply#

CodeMeans
0Nothing needed changing, or the operations were staged, or they were committed and confirmed.
1No usable licence; no --agent-url and no --commit; --commit without --confirm-minutes; the agent refused the staging request; the commit failed; or the confirm did not reach the router.

There is no 2 here.

The installer#

wheelhouse-install:

CodeWhen
0Installed. Also --commands, which prints and stops. Also choosing "keep running the live system" from the menu.
1Under --answers: DISK missing, both passwords missing, the answers file unreadable, --commands without --answers, the system is already installed, or the install failed and ON_ERROR is exit.

Interactively, a failed install returns to the menu rather than exiting, and the exit status is whatever the eventual menu choice produces. Under --answers the failure path is ON_ERROR — which may halt or power off the machine instead of returning a status at all.

wheelhouse-autoinstall:

CodeMeans
0A seed was found. Its converted answer file's path is on stdout.
1No seed, or the search was switched off with wheelhouse.autoinstall=off.

It writes to no disk.

install-driver.py, which drives the base installer through a pseudo-terminal:

CodeMeans
0The image was written.
1It was not — including an unknown prompt, which stops rather than guesses.

The helper scripts#

Program012
wait-for-vyos.shthe router finished loading its configurationit failed, or five minutes passed—
firstboot.shthe key exists, or was createdthe key could not be generated, or did not reach the configuration—
console-banner.shalways——
seed-to-answers.pyconvertedthe seed is not a mapping, or has no wheelhouse: sectionwrong number of arguments

The tools#

Tool012
install.shinstalled, verified, or rolled backany die: a missing flag, no Go and no binary, an unknown verb, a failed health check, a rollback with nothing to roll back to—
sign.shthe manifest was written, the files were signed, or verification passedany die: checksums do not match, no signing key, a signature that does not verify, or REQUIRE_SIGNATURE=1 with nothing signedno verb, -h or --help
check-images.pyevery image is well formed and, without --offline, reachableany malformed or unreachable image — and, with --require-digest, any floating tag—
opnsense-import.pythe import ran; with --check-against, nothing is missingwith --check-against, something is missing; or the file is not valid XML or not an OPNsense configuration—
wheelhouse-seed.pythe seed was writtenno password and nowhere to ask for one, the two entries differ, the importer failed, or no ISO tool is installed—
build-deb.pythe package was written--ui is not a built UI—
release-index.pythe index was written, or an older tag was left alonethe tag directory is missing, holds no artefacts, or is not a vX.Y.Z tag—
build-iso.shthe image was builtany failing step — it runs under set -euo pipefail, and several checks fail when upstream has changed—

opnsense-import.py --check-against exits 1 when something is missing, which suits a cron job run after a migration.

HTTP, not exit codes#

An API caller gets a status code, not an exit status. Those are on Status codes. They do not line up: 402 has no command-line equivalent, and a plan exit of 2 has no HTTP one.

See also#

Updated 2026-09-02 cli exit-codes automation