Skip to main content

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 own ok: true stays (the command ran and produced a verdict); the verdict is data.ok. The findings are in data.checks[].findings[]. There is no error member.
  • exit 6, set after the envelope is rendered.
  • A green verdict has data.ok: true, data.slug: null and 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 own ok: true stays (the command ran to a defined end and the gateway is back on its snapshot); the verdict is data.ok. There is no error member.
  • data.cause is one of lock_expired, write_failed, scan_incomplete or verify_failed; the run directory's events.ndjson and result.json carry 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), and data.warnings holds post_commit_error (<step>: <code>).
  • exit 6, set after the envelope is rendered.
  • A successful apply has data.outcome applied, data.ok: true, data.slug: null, data.cause: null and exits 0; an empty plan is outcome noop, 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 own ok: true stays (the command ran to a defined end and the master is deployed); the verdict is data.ok. There is no error member.
  • data.backup.reason is 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's lastSyncTimestamp did not advance past the value sampled once the scan-lock was held, just before the first write) and content_differs (the backup is syncing but its files or its REST read-back differ from the render).
  • A backup with no ssh or local transport in nodes.backup is checked over REST only: its convergence rests on the timestamp, the mode and the REST read-back, and the file comparison is a warn.
  • 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.outcome applied, data.ok: true, data.slug: null and exits 0; a target without nodes.backup has data.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 own ok: true stays (the command ran to a defined end and the import was sent); the verdict is data.ok. There is no error member, and result.json in 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_command is 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 guarded project 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 is null when the project was created by the run (overwrite=false), because no pre-image exists to go back to.
  • A verified import has data.outcome imported (or created), data.ok: true, data.slug: null and restore_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.