Exit codes and messages
The six-code exit contract for script authors, the message convention, verbose output, and the bug-report capture.
Every flitz failure branches on the same small set of exit codes. This page
is the one place a script author needs.
Exit codes
flitz reports exactly six codes, so a script can branch on the class of
failure without matching prose:
| Code | Class | Meaning |
|---|---|---|
| 0 | Success | The command did what it was asked. |
| 1 | Usage or configuration | The invocation, or the project/machine/organization state it names, is wrong and the fix is yours: an unknown command or flag, a missing or malformed argument, --json on a command that does not support it, a required confirmation that could not be obtained non-interactively, a missing or malformed pin or flitz.yaml, a release with no SDK for this machine, an unsatisfiable plugin resolution, or a refusal that is yours to fix: an archived, deleted, or unknown app, an app key another app already holds, an unknown publish id, or a new app you did not confirm. |
| 2 | General or unexpected | The catch-all: a local filesystem or permission failure, an unresolvable home directory, a lock that could not be acquired, an interrupted command, or an unexpected error. |
| 3 | Not authorized | No valid credential, a rejected or expired one, an inactive organization license, or an interactive-only operation attempted under an API key. |
| 4 | Service or network | The service or a link was unreachable, timed out, returned a persistent error or a response the CLI could not read; an upload failed; or a write-once destination was already occupied. |
| 5 | Build | The app failed to compile: the kernel compilation failed or bytecode compilation failed step. |
A build failure is code 5 whichever of the two compile steps produced it; the message names the step. See Troubleshooting for what each one means.
A flitz.yaml that is not valid YAML, is not a mapping, or gives a key a
non-string value is a usage error (exit 1) naming the file. Fix it, or delete
it and re-run.
One subtlety around publish: code 2 is the only code that can follow a
publish whose bundle upload succeeded. Once the URLs are printed, a later local
failure — a --qr-output PNG that could not be written — reports it and exits
2 without retracting the result.
Reading the code in a script
if flitz status --json > status.json; then
jq -r '.checks[] | select(.verdict=="blocking") | .message' status.json
fi
flitz publish --json > result.json
case $? in
0) deploy "$(jq -r .bundle_url result.json)" ;;
3) echo "not authorized — check FLITZ_APIKEY" >&2 ;;
5) echo "build failed — see the log above" >&2 ;;
*) echo "publish failed (exit $?)" >&2 ;;
esacA --json command writes no
JSON on failure. Check the exit status first, then parse stdout only on 0. The
one exception is a publish --qr-output failure: it exits 2 with the complete
document already on stdout. flitz status always exits 0 — its
verdicts live in the check markers, not the exit status.
Message convention
Most errors and warnings are written to stderr in one shape: the verdict
marker and a cause, then one indented → line per next action.
✗ <cause, a lowercase sentence naming the concrete value involved>
→ <the next command to run>In plain output the marker is its ASCII form:
[fail] <cause>
-> <next command>What you can rely on:
- The cause names the concrete value that matters — the tag, the path, the host, the HTTP status.
- A hint names the next thing to run. A cause with no useful next step carries none.
- Compiler output passes through verbatim. When a compile step fails, its raw output is printed unreformatted beneath a framing line naming the tool; the CLI does not parse, re-wrap, or summarize it.
- Secrets are redacted in error causes, in the bug-report capture, and in
tool output quoted on failure: credentials, the API key, plugin tokens, and
signed URLs. A signed URL keeps its origin and path with its query replaced
by
?<redacted>, and an echoed build command keeps each--dart-definekey with its value replaced by<redacted>.
Some messages use a different shape. Argument errors print the message followed by the command's usage text, a few failures are a single plain line without a marker, and a cause can quote a lower-level error verbatim.
Warnings use the ⚠ marker in the same shape and never change the exit code.
Verbose output (-v)
-v (or --verbose) has exactly one level and four effects:
- Stack traces on error — every error additionally prints its stack trace beneath the cause.
- Live build output — the compile steps stream their output to stderr as it arrives, instead of being captured and only quoted on failure. A long compile stops looking like a frozen spinner.
- Fuller
flitz status— rows thatstatusotherwise hides are shown; seeflitz status. - Retry announcements — a retried network request says so.
-v does not log HTTP requests, does not dump environment variables, and does
not enable a log file. A second -v (-vv) is a usage error — there is only
one level.
Verbose output is not redacted
What -v streams live, and the stack traces it adds, are printed as they
are. Read them before you paste them anywhere.
Without -v, build-tool output is captured: discarded on success, and printed
above the error on failure — its last 64 KiB, with --dart-define values
redacted. Redaction also replaces long, token-like runs of letters and digits,
so a very long identifier in quoted compiler output can show as <redacted>;
re-run with -v to see it unmodified.
The bug-report capture
When the CLI hits something it does not recognize — and only then — it prints
✗ unexpected error — please report this
<exception type>: <message>followed by a delimited capture block, and exits 2. A recognized, cleanly handled error never emits one; the block exists precisely because the CLI does not know what went wrong.
The block is built from an allowlist — it contains exactly these facts and nothing swept from your environment or filesystem:
| Field | Contents |
|---|---|
| CLI | Version and host (for example linux-x64). |
| OS | Operating system and version. |
| Project | The pinned tag (or that none is pinned), whether it is installed, and how many SDKs are cached. |
| Invocation | The command line. Subcommands, positional arguments (tags, shell names, app keys, publish ids), and flag names are kept; flag values are replaced by <redacted>, except the values of --dir, --target, --qr-output, --dart-define-from-file, and --schema. |
| Auth | Booleans only: whether an interactive sign-in is stored and whether FLITZ_APIKEY is set. The license shows as unknown. Never a token, never an identity. |
| Error | The exception type, its message, and the stack trace. |
It never contains the contents of credentials.json, config.yaml,
~/.netrc, or gradle.properties; never a request or response header; never
a signed URL. A final redaction pass runs over the whole block as a backstop.
The block is written to stderr only, is never saved to a file, and is never transmitted anywhere. It is built to be safe to paste into a bug report; glance over it before you share it. To report it, contact support and send the block with what you ran.
Interrupting a command
Ctrl-C is handled. The CLI writes one line naming the phase it interrupted,
terminates any build tool it started rather than orphaning it, deletes its
scratch directory, releases any lock it holds, and exits 2. A resumable SDK
download is deliberately kept, so re-running continues rather than
restarting. An interrupted publish shows in the
publish history as pending, and as
abandoned an hour after it started, when only
flitz publishes --all lists it. Re-running
publish starts a fresh one. A second Ctrl-C leaves immediately, still exit 2.