Wheelhouse docs

Exit codes#

A status code is the interface a script actually uses, so each of these is deliberate. The one worth memorising is wheelhouse-agent plan: exit 2 means the router has drifted, the way diff -q behaves, so it drops into a shell test without parsing text.

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 directoryanother 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

Note the asymmetry: 2 is not a failure, it is an answer. A script that treats every non-zero status as an error will report 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. apply either did the work or did not.

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.

That is its whole interface. 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 deliberately when upstream has changed

opnsense-import.py --check-against exiting 1 for "something is missing" is the point of it: it is meant to be a cron job after a migration.

HTTP, not exit codes#

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

See also#

Checked against#

agent/main.go (main, registerFlags, runDaemon, loadSecretFiles), agent/desired.go (runPlanApply), agent/store.go (errStoreLocked, errStateTooNew), agent/oidc.go (validateOIDCFlags), agent/security.go (recoverPanics), packaging/wheelhouse-install, packaging/wheelhouse-autoinstall, packaging/install-driver.py, packaging/seed-to-answers.py, packaging/wait-for-vyos.sh, packaging/firstboot.sh, packaging/console-banner.sh, packaging/build-deb.py, packaging/iso/build-iso.sh, install/install.sh, install/sign.sh, scripts/check-images.py, tools/opnsense-import.py, tools/wheelhouse-seed.py, .forgejo/workflows/release-index.py.

Updated 2026-09-02 cli exit-codes automation