Exit codes¶
Stable, and depended on by systemd units, CI pipelines and operator scripts. Codes are never renamed or repurposed once published.
The mapping happens in exactly one place — domain.ExitCode — so a new error
type cannot quietly acquire a new exit code by being handled somewhere else.
| Code | Error code | Meaning |
|---|---|---|
| 0 | — | Success. |
| 1 | internal |
Internal or unexpected error. The manager itself is at fault. |
| 2 | usage |
Usage or input validation. A mistyped flag, an unknown subcommand, an invalid manifest. |
| 3 | preflight |
A precondition failed: a missing tool, a version below what the release requires, not enough disk. |
| 4 | locked |
The deployment lock is held by another operation. Retry, or pass --wait. |
| 5 | installation |
The installation is missing or corrupted. |
| 6 | secrets |
A secrets error: the state could not be decrypted, a required secret is absent. |
| 7 | runtime |
The container runtime failed — docker or docker compose. |
| 8 | health |
A health check or the release's smoke test failed. |
| 9 | incompatible |
The release cannot be installed over, or rolled back to, what is running. |
| 10 | backup |
A backup or restore failed. |
| 11 | compensated |
The operation failed and compensation succeeded. The system is back where it started. |
| 12 | manual-intervention |
The operation mutated something it could not undo. A human has to look. |
| 130 | interrupted |
Interrupted by a signal. |
12 is the one that matters¶
Exit 12 means an operation changed something no automatic action can put back — a migration that ran, a restore that got halfway. It is not a louder version of 11: 11 says the system is where you left it, and 12 says it is not.
The systemd unit sets RestartPreventExitStatus=12 so a unit that hits it does
not spin. The failed operation keeps surfacing in status and doctor until an
operator clears it with morzer status --clear-intervention.
Steps declare this explicitly rather than having it inferred from the absence of a compensator. Most steps without one are simply read-only, and inferring would flag every failed health check for human acknowledgement — which trains people to clear the flag without looking, destroying the value of the one signal meant to stop them.
Error codes in output¶
Every failure carries a machine-readable code (the middle column above)
alongside a human message and a hint. Under --json they are fields of the
single object on stdout:
{
"ok": false,
"exit_code": 3,
"error": {
"code": "preflight",
"category": "user",
"message": "docker 23.0.1 is older than the 24 this release requires",
"hint": "upgrade docker, or install a release with a lower requirement"
}
}
Match on code, not on message. Messages are written for people and will be
reworded; codes are a contract.
Categories¶
category groups a failure by who can act on it. It carries no control-flow
meaning — it exists so a dashboard can separate "the operator mistyped
something" from "the manager has a bug".
| Category | Meaning |
|---|---|
user |
The operator can fix this directly. |
system |
The machine or an external tool is at fault. |
bug |
The manager itself is at fault. |
conflict |
Another actor holds the resource. |
Every error carries a hint¶
message says what happened; hint says what to do about it. Both are printed.
An error without its remedy is a support ticket.
Neither may contain a secret value — callers redact before constructing, and the
domain.Secret type cannot be formatted into one by accident.