Machine contracts

Dino's machine interfaces: the exit-code contract (0/2/3/4/5/6/70), the versioned DinoResult JSON output, and the stderr error envelope.

Dino's output is a set of product interfaces, not incidental formatting. An agent or CI job branches on the exit code, parses the JSON result, and recovers from the error envelope, all without reading prose. This page is the whole contract on one screen.

Exit codes

Dino maps every command's outcome to one of eight codes, so you can branch on $? without parsing output. The mapping is stable across commands and versions.

CodeKindMeaningRetryable
0cleanVerification ran and passed. No gate failed.n/a
2usageBad invocation, unknown flag, missing argument, malformed input.permanent
3policyA policy gate failed (--fail-on-high, --fail-on-breaking, --fail-on-undocumented, watch enforce). The "do not ship" signal.permanent
4transientRetryable environment failure, DNS, connection reset, rate limit, 429/502/503/504.transient
5configInvalid configuration, malformed .dino.yml, schema violation, unresolvable target.permanent
6partialScan completed with reduced coverage. Evidence is incomplete.permanent
70crashUnexpected internal error. Worth surfacing to a human.permanent

Policy gates exit 3, not 1. When more than one outcome applies, the highest-precedence kind wins: crash > config > usage > transient > policy > partial > clean. So a run that fails a gate (3) and hits a transient error (4) exits 4. A degraded run's gate result is not trustworthy.

Pass --accept-partial to downgrade a partial outcome (6) to 0. It affects only 6, never policy, transient, config, usage, or crash.

dino scan --endpoint "$URL" --fail-on-high --format json --quiet
case $? in
  0)  echo "clean, continue" ;;
  3)  echo "policy gate failed, do not ship" ;;
  6)  echo "partial coverage, decide (or use --accept-partial)" ;;
  4)  echo "transient, back off and retry" ;;
  2|5) echo "usage/config, fix input, do not retry as-is" ;;
  70) echo "crash, surface to a human" ;;
esac

JSON output

Every command that produces a result can emit structured JSON with --format json. The output is versioned and validated, not a pretty-printed dump, and stdout carries only the JSON object, so it pipes into jq.

dino scan --format json → DinoResult

{
  "dinoResult": "1.0",
  "identity": { "runId": "…", "tenantId": "…", "environment": "…", "generatedAt": "2026-08-13T10:00:00.000Z", "targets": { … } },
  "scope": { "scopeState": "KNOWN", "snapshots": [ … ], "gaps": [ … ] },
  "verification": { "tools": [ /* one record per tool: ran / excluded / unavailable / not-selected */ ], "durationMs": 0, "targetUnreachable": false },
  "operations": [ /* per-operation rows: tools, worst severity, health, execution coverage */ ],
  "findings": [ /* grouped findings: target, tool, classification, normalizedLevel, count, examples */ ],
  "verdict": { "coverage": "full", "operationCount": 19, "overallSeverity": "HIGH", "health": { "score": 62, "level": "HIGH", "verdict": "At risk" }, "reasons": [] }
}

The published schema is dino-result.v1.json. stdout is the canonical serialization (sorted keys, no whitespace) plus one newline: the same bytes the Dino runner posts to the cloud and an attestation signs, so sha256 of the document is its digest. Every object is strict (no undeclared fields) and the cross-field partitions are asserted on parse, so a malformed result is never printed.

The verdict. verdict.health rolls the API up to one word. The level cascades by worst severity; the verdict is the display word:

levelverdict
CRITICALCritical
HIGHAt risk
MEDIUM / LOWNeeds attention
CLEANHealthy
UNTESTEDUntested (score is null)

score is 0–100, or null when withheld: the level is UNTESTED, coverage is partial, or no operation was scored. An empty scope is Untested, never Healthy. Dino never reports a clean bill of health for an API it did not test.

Deterministic bytes. Ordering is total and code-unit based, so two runs on the same evidence under the same policy produce the same document apart from identity.runId and the captured instants. To check for a regression, diff the operations and findings.

The partial signal. verdict.coverage is "partial" whenever verification was incomplete: an unknown scope, an unavailable or cancelled tool, unadjudicated planned work, a degraded run. verdict.reasons names the causes. The scan still emits a valid DinoResult; it exits 6 unless you pass --accept-partial (an unreachable target is 4, transient).

Evidence is off by default. Request/response bodies are omitted unless explicitly enabled. When included, secret-shaped values (keys, JWTs, emails, card-like numbers, Basic auth) redact to [REDACTED], non-allowlisted headers are redacted, and large fields are size-capped.

dino docs --format json → dino-api-docs

A different shape with a stable formatName discriminator so a consumer can tell it apart from a scan result:

{ "formatName": "dino-api-docs", "formatVersion": 1, "title": "…", "operationCount": 19, "health": { … }, "operations": [ … ], "coverage": "full" }

Always branch on formatName (dino-api-docs) versus dinoResult (scan) before reading a JSON file. Never assume which command produced it.

Error envelopes

When a command fails with an error (not a verification result), Dino writes a single-line JSON envelope as the last line of stderr. It is emitted on exit 2, 4, 5, and 70, never on 0, 3, or 6, which are results.

{"error":{"kind":"transient","message":"Target rate-limited","retryable":"transient","exitCode":4,"suggestion":"Retry after a short backoff"}}
FieldTypeMeaning
kindstringShort machine label for the error class (e.g. rate_limited, invalid_config, usage, crash).
messagestringHuman-readable, sanitized, single-line.
retryable"transient" | "permanent"Whether retrying the same call could succeed.
exitCodenumberMirrors $? (2/4/5/70).
inputany (optional)Offending input, echoed with secrets stripped.
suggestionstring (optional)A recovery next step, when Dino has one.

Branch on retryable, not on message text. Both message and any echoed input pass through a sanitizer, so a secret-bearing flag (e.g. --token sk-live-…) can never reach the envelope. It is safe to log. Because it is the last stderr line, an agent can read it even when earlier stderr lines contain progress output.