Output contract (for agents)
Success prints a stable envelope on stdout; errors go to stderr in both modes (exit code carries the same signal):
- Success:
{"ok": true, "profile": <name|null>, "data": {...}} - Failure:
{"ok": false, "profile": <name|null>, "error": {"code": "<slug>", "message": "...", "endpoint": <url|null>, "hint": "..."}}
--compact implies --json and renders the same envelope on a single line.
Two documented exceptions: clap usage errors exit 2 and are rendered by clap
itself (not JSON); the exit-2 class also covers destructive operations run
without --yes.
One success-path exception: ign completions <SHELL> writes the raw
completion script to stdout regardless of --json — shells source its
output directly, so it is never JSON-wrapped.
The ign api call contract exception
ign api call is the one documented exception on the SUCCESS side:
its data.result.data is the gateway's OWN JSON — gateway-verbatim
(no field dropped, no value coerced, key order preserved), not a
curated model. A 2xx answer with a non-JSON body refuses exit 1
internal with an explanatory message — binary endpoints belong to
the logs/backup download pipelines, never this command. Gateway error
bodies on the api-call path ride the error envelope's message
verbatim, truncated at 4 KiB with an explicit ... [truncated]
marker. Auth-pattern headers (Authorization, X-Ignition-API-Token,
Cookie — case-insensitive) are refused exit 2 BEFORE any request:
credentials come from the profile, full stop.
The MCP refusal-prose exception (2026-09-16)
The frozen envelope governs the CLI's own stdout/stderr only —
byte-for-byte, always. Over the MCP transport (ign mcp serve), a
confirmation_required refusal is the SECOND dated envelope-transport
exception (alongside ign api call above): its message/hint PROSE
adapts to the transport — the JSON names confirm: true (the
MCP-native confirmation) and marks --yes/IGNITION_YES=1 CLI-only —
while the shape stays byte-stable (same keys, same order,
ok:false/code/profile:null/endpoint:null untouched). The MCP
input schema correspondingly keeps confirm OUT of required for
guarded verbs: schema-required would invite agents to auto-fill
confirm:true, so omission is legal and answered by the refusal.
The deploy verify verdict exception (2026-10-14)
ign deploy verify is the THIRD documented success-path exit exception (after
ign lint --strict and ign testing run), and the first in the deploy
family: deploy apply and the redundant-pair checks of later phases will
reuse the same envelope. A red verdict is not an error. The command prints
the frozen success envelope, then ends with exit 6 (target state):
- stdout:
{"ok": true, "profile": "<master profile>", "data": {..., "ok": false, "slug": "deploy_verify_failed", "checks": [...]}}. The envelope's ownok: truestays (the command ran and produced a verdict); the verdict isdata.ok. The findings are indata.checks[].findings[]. There is noerrormember. - exit 6, set after the envelope is rendered.
- A green verdict has
data.ok: true,data.slug: nulland exits 0.
ign deploy verify --node backup|both uses the same verdict shape: the backup
node's checks are in data.backup and the secret-name check of each node in
data.secrets, and a red result there is data.ok: false,
deploy_verify_failed and exit 6 after the envelope.
deploy_verify_failed lives only in data.slug. It is not a CoreError slug
and is never a row of the exit-code table below; an agent that wants the
verdict reads data.ok, and one that only looks at the exit code sees 6. The
rule exists because the error envelope has no data field, and a verdict is
only useful with its findings.
Failures before a verdict exists stay ordinary error envelopes on stderr
with their own exit codes and an empty stdout: an unreachable gateway
(network_error, exit 4), an unknown or secretless profile (exit 3), an
unreadable host (transport_failed, exit 4), a refused tree (exit 2), and a
gateway that refuses the scan, mode or resource reads. Over MCP the same
envelope is returned with isError false, because the envelope's ok is
true.
The deploy apply rolled-back verdict exception (2026-10-18)
ign deploy apply is the FOURTH documented success-path exit exception, and it
reuses the deploy verify shape above. An apply whose post-write verify is red,
whose lock lapsed, whose scan did not complete or whose write failed has
restored the pre-apply snapshot and scanned. That is a finished run with a
verdict, not an error, so the command prints the frozen success envelope and
then ends with exit 6 (target state), as Decision A of the v1.6 milestone
fixed:
- stdout:
{"ok": true, "profile": "<master profile>", "data": {..., "outcome": "rolled_back", "ok": false, "slug": "deploy_rolled_back", "cause": "<cause>"}}. The envelope's ownok: truestays (the command ran to a defined end and the gateway is back on its snapshot); the verdict isdata.ok. There is noerrormember. data.causeis one oflock_expired,write_failed,scan_incompleteorverify_failed; the run directory'sevents.ndjsonandresult.jsoncarry the detail. A step after the commit that cannot be answered at all (the lock read, the scan or the verify fails with a gateway error) is restored like a red verify with the closest cause (lock_expired,scan_incomplete,verify_failed), anddata.warningsholdspost_commit_error (<step>: <code>).- exit 6, set after the envelope is rendered.
- A successful apply has
data.outcomeapplied,data.ok: true,data.slug: null,data.cause: nulland exits 0; an empty plan is outcomenoop, exits 0 and touched nothing.
deploy_rolled_back lives only in data.slug. It is not a CoreError slug and
is never a row of the exit-code table below. A caller that only checks the
envelope's ok would read a rolled-back apply as a success, so an agent that
applies must read data.ok (or data.outcome); one that only looks at the exit
code sees 6.
Failures before the write, and a rollback that could not restore, are
ordinary error envelopes on stderr with their own exit codes and an empty
stdout: plan_stale, destructive_change_refused, deploy_blocked,
deploy_preflight_failed, deploy_journal_present and rollback_failed are
rows of the table below. The verb is not in the MCP catalog.
The deploy apply backup-unsynced verdict exception (2026-10-20)
ign deploy apply to a redundant pair has a second verdict, and it is the FIFTH
documented success-path exit exception (after ign lint --strict,
ign testing run, deploy verify and the rolled-back apply above). The master
applied and verified, but the backup node could not be proven to hold the same
files within --backup-timeout. That is a finished run with a verdict, not an
error, so the command prints the frozen success envelope and then ends with
exit 6 (target state), as Decision A of the v1.6 milestone fixed:
- stdout:
{"ok": true, "profile": "<master profile>", "data": {..., "outcome": "applied_backup_unsynced", "ok": false, "slug": "deploy_backup_unsynced", "cause": null, "backup": {"converged": false, "reason": "<reason>", ...}}}. The envelope's ownok: truestays (the command ran to a defined end and the master is deployed); the verdict isdata.ok. There is noerrormember. data.backup.reasonis one of four words, ranked in this order when more than one is true on the final observation:unreachable(a backup REST call or file read could not be answered; the check detail carries the error code),sync_exhausted(the backup logged that it gave up syncing during this run and will not catch up by waiting; a line logged before the run began does not count, and the CLI host's clock and the gateway's are assumed to agree to within a few seconds),not_syncing(the pair link is down or the backup'slastSyncTimestampdid not advance past the value sampled once the scan-lock was held, just before the first write) andcontent_differs(the backup is syncing but its files or its REST read-back differ from the render).- A backup with no
sshorlocaltransport innodes.backupis checked over REST only: its convergence rests on the timestamp, the mode and the REST read-back, and the file comparison is awarn. - exit 6, set after the envelope is rendered.
- The master is not rolled back: its run record stays
applied, its files are the verified render, and nothing is restored. The only write the backup check makes is one resync request to the master (data.backup.resync). - A converged backup has
data.outcomeapplied,data.ok: true,data.slug: nulland exits 0; a target withoutnodes.backuphasdata.backup: null.
deploy_backup_unsynced lives only in data.slug. It is not a CoreError slug
and is never a row of the exit-code table below. A caller that only checks the
envelope's ok would read an unsynced backup as a success, so an agent that
applies must read data.ok (or data.outcome); one that only looks at the exit
code sees 6, the same code as a rolled-back apply, and tells them apart by
data.outcome.
A run killed during the backup wait (Ctrl-C, a lost terminal) has an applied
master and no verdict. Follow it with
ign deploy verify --env <ENV> --node backup to see whether the backup caught up.
The deploy projects unverified verdict exception (2026-10-23)
ign deploy projects has a verdict of its own, and it is the SIXTH documented
success-path exit exception (after ign lint --strict, ign testing run,
deploy verify, the rolled-back apply and the backup-unsynced apply above). The
artifact was imported, but the project read back from the gateway does not equal
the artifact by canonical hash, or could not be read back or compared at all.
That is a finished run with a verdict, not an error, so the command prints the
frozen success envelope and then ends with exit 6 (target state), as
Decision A of the v1.6 milestone fixed:
- stdout:
{"ok": true, "profile": "<profile>", "data": {..., "outcome": "imported_unverified", "ok": false, "slug": "deploy_project_unverified", "restore_command": "<command>"|null, ...}}. The envelope's ownok: truestays (the command ran to a defined end and the import was sent); the verdict isdata.ok. There is noerrormember, andresult.jsonin the run directory holds the same fields. - exit 6, set after the envelope is rendered.
- No release record is written, no second import is sent and no further gateway request is made. Nothing is restored automatically: the project the gateway holds is what the import left, and the saved pre-image stays in the run directory.
data.restore_commandis the command that puts the saved pre-image back:ign project import <name> --file <run_dir>/export/<name>.zip --collision-policy overwrite --yes. It is a normal guardedproject import; the operator reviews and runs it. The path is relative to the repo root (where the run directory lives), the command carries no--profile, and the human output names the gateway it is meant for, so run it from the repo root with the profile for that gateway. It isnullwhen the project was created by the run (overwrite=false), because no pre-image exists to go back to.- A verified import has
data.outcomeimported(orcreated),data.ok: true,data.slug: nullandrestore_command: null, and exits 0.
deploy_project_unverified lives only in data.slug. It is not a CoreError
slug and is never a row of the exit-code table below. A caller that only checks
the envelope's ok would read an unverified import as a success, so an agent
that deploys must read data.ok (or data.outcome); one that only looks at the
exit code sees 6, the same code as the other verdicts and as
project_hash_mismatch, and tells them apart by data.outcome and data.slug.