Skip to content
downpipes docs

downpipe exit codes and verification outcomes

The downpipe reader commands finish with a process exit code that says exactly what was proven. This page is the normative contract for those codes, for a break-glass recoverer or a drill script that needs to react to a verdict rather than read prose. It is authored from the exit constants in the reader itself, not from older recovery notes.

The codes are stable and machine-readable, so a script can branch on them with confidence. A clean run exits zero; every non-zero code names one specific failure class, and the tool also prints a human-readable verdict to stderr alongside it. The two reading-and-checking commands that carry this contract are verify and restore; the command reference for both is the CLI command reference.

The normative reader exit codes

These six codes are the SPEC-normative reader set. The reader maps an internal exit error to its process exit status, so the code you observe is the precise verdict the verification produced.

Code Meaning When the reader returns it What to do
0 Verified and complete The run passed every check: signature, recipient set, key commitment, shard hashes, declared count, Merkle root and RUNLOG freshness, with no per-record problem Trust the result. Keep the signed receipt as evidence of the drill or restore
2 Unverified A missing, invalid, single-half or wrong-signer signature, a failed integrity recomputation, a failed break-glass binding, or a failed --check-bundle, with no override given Stop and investigate. Confirm you pinned the correct signer public key and supplied the right identity before considering the salvage path below
3 Incomplete coverage The verified coverage is below the run’s declared record count, so the run is not whole even though what is present checks out Treat the run as not whole. Check whether the bucket copy is complete and whether every shard is present, then re-run
4 Plaintext hash mismatch A per-record plaintext failed to match its signed hash during restore, which means the archived bytes for that record do not match what was signed Treat the archive as altered for that record. Do not rely on the restored value; investigate the destination bytes
5 Freshness or rollback problem A stale run (not the latest for its downpipe), a maximum RUNLOG index below your --min-runlog-index pin, a RUNLOG that verified against your signer and then contradicted itself, or a RUNLOG that could not be checked at all because it is absent, unreadable, unparseable, empty, does not carry this run, fails to verify against your signer, or disagrees with the signed root manifest Read the stderr line, which names which of these the reader found. The first two are statements about the run’s age and are acknowledged with --allow-stale; the other two are statements about the RUNLOG itself and are acknowledged with --allow-unverified-runlog. Prefer finding the latest good run over either
6 Usage or input error A bad or missing flag, an unparseable run id, a missing source, or a missing, unreadable or wrong-kind identity, signer or receipt-signer key file Fix the command. The stderr message names the missing or invalid input

The checks behind code 0 run read-only on verify and run again, value by value, on a restore --apply, so a restore that writes also re-proves the run as it goes and a per-record mismatch surfaces as code 4 rather than a silent partial write.

Code 1, the uncoded fallback

Code Meaning When the reader returns it What to do
1 An I/O, connectivity or unexpected failure An archive object could not be read at all: a missing or unreadable manifest, a partially-downloaded archive, or an internal fault the reader did not tag with one of the codes above Not a verdict on the archive’s authenticity. Read the printed message, fix the underlying problem, and retry

Code 1 sits outside the six-code SPEC-normative set, the same way 7 to 13 do, but for the opposite reason: it is not a classified verdict at all, it is what the reader returns when nothing else applies. A restore --apply that writes some records but hits at least one per-record failure still exits its highest normative per-record code (4 for a plaintext-hash mismatch, 2 for a structural or AEAD failure); 1 is reserved for the case where every failed record’s cause was itself uncoded, for example a missing segment object from a partially-downloaded archive. Unlike the hard-failure codes 2 to 6, worth a retry once the access problem is fixed rather than an escalation. A destination that could not be reached at all (a network or DNS failure, a refused connection, or a non-2xx status before any bytes arrived) is a distinct case with its own code: see code 11 below.

Code 11, the destination-unreachable code

Code Meaning When the reader returns it What to do
11 The destination could not be reached A verify, attest, restore, inspect, keys or prune read of the root manifest, its signature, the RUNLOG, a shard or a segment failed at the transport or access layer over --s3-endpoint: a network or DNS failure, a refused connection, or the destination returned a non-2xx status (401/403/404/5xx) before any bytes came back Not a verdict on the archive. Its bytes were never retrieved, so nothing about it is yet known. Check the endpoint, bucket and credentials named in the printed message, then retry

Code 11 sits outside the six-code SPEC-normative set, the same way 7 to 10, 12 and 13 do, added for the same reason each of those was: the reader grew a distinction its original six codes had no room for. It splits a case code 2 used to cover: whether an object’s bytes were RETRIEVED at all. Code 2 means bytes were retrieved and then failed a hash, signature or authenticated-decrypt check, a real reason to stop and treat the archive as suspect. Code 11 means the request never got bytes back at all, most often a wrong --s3-endpoint or --s3-bucket, or an expired AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY, worth a retry once fixed rather than an escalation. Before this code existed the two shared 2, leaving a recovery script running mid-incident unable to tell “retry once the endpoint or credentials are fixed” from “stop, this archive may be compromised”, the opposite responses the two causes demand.

A truncated or oversized read stays on the 2 side, not 11: some bytes did arrive, so something is now known about them (an AEAD tag or a signed hash to check them against), and a partial or oversized object is at least as consistent with tampering or corruption as with a network blip. A read failure the reader cannot positively attribute to the destination also stays on whichever code it already carried before code 11 existed, most often 2: the deliberately safer default, since an unclassifiable failure reads as a possible tamper finding, never as a silent invitation to retry.

Code 11 is reachable only over --s3-endpoint. verify, attest, restore, inspect, keys and prune all honour it on the same boundary. A local --archive read failure is never reclassified to 11: a local “no such file” or “permission denied” carries far less signal than a network 403 or 404 about whether an object is merely inaccessible or genuinely gone, so it keeps whatever code it already carried, most often 2 (see also the pruned-run wording in the CLI command reference for prune, which reasons through the same ambiguity for a locally absent run tree).

The provisioner code, outside the normative set

preflight is on the provisioner path, not the reader path, so it deliberately uses a code outside the normative reader set rather than reusing one.

Code Meaning When preflight returns it What to do
7 Preflight failed One or more deploy-time account checks failed, or an explicitly requested --domain could not be verified against an active zone Resolve the failing checks (or the zone read) before you deploy. Run with --json to feed the result to the console or a script

preflight keeps 7 distinct from 2 on purpose: a deploy-time account check failing is a different thing from a reader’s signature or recompute verdict, and exit-status-driven tooling can tell them apart. A check the token cannot read degrades to “unknown” rather than failing the run, but an invalid token aborts loudly, and a plain usage error from preflight is still the normative 6.

The three restore advisories, also outside the normative set

verify --deep and restore --apply can return three further codes that sit outside the six normative reader codes and outside preflight’s 7. A fourth, 9, is returned by any command that accepts custody artefacts and is not an advisory at all.

Code Meaning When the reader returns it What to do
8 Verified or restored, but contains incompleteness markers The run verified (verify --deep) or restored (restore --apply) cleanly, with every present value hash-checking, but one or more records are incompleteness-marker placeholders: the source was only partially available when the run was written, so that record’s value is a sentinel, not the source’s live data Treat the data as genuinely restored, but check the printed per-record warning (or the receipt’s incompleteMarkers and incompleteMarkerKinds) to find which records are placeholders before relying on their value
9 Custody integrity: the shares or envelope are wrong A command was given custody artefacts (--share or --wrapping-key with --envelope) instead of --identity, and the key could not be recovered from them: the recombined wrapping key failed its public checksum, the authenticated envelope decrypt refused, or the recovered plaintext is not an identity-key file. Nothing was read from the archive, because no key was recovered to read it with Check you have supplied enough DISTINCT shares to meet the ceremony’s threshold, and that the envelope is the one those shares belong to. This says nothing about the archive, which has not been touched
10 Restored, but some records were not written The run verified and every record the target could take was written and hash-checked, but one or more were skipped because the target could not take them: a name this target cannot represent (a Windows reserved device name such as CON on the file sink), a destination key that already exists, or two records mapping to the same key. Each skipped record is named on stderr, and the signed receipt carries the machine-readable tally as recordsUnwritten with a per-kind unwrittenKinds breakdown over unrepresentable, existing and collision Those records are in the archive and are not on your disk. Read the printed conflicts, then re-run into an empty target, or restore the named records with --sink env or --sink discard plus your own write step. The archive is not damaged and the data is still recoverable
12 Restored, but this release cannot render some bodies The run verified, every planned record was written and every value hash-checked, but one or more records carry a D1 body-format label this release does not know how to turn into runnable SQL. The verified bytes were written unchanged Nothing is lost and nothing is corrupt, so keep the written file. Do not feed it to sqlite3 on the strength of the D1 replay guidance, because it is not SQL. Restore again with a release that knows the label, which is normally a newer one, since the label is versioned independently of the archive

Code 8 is deliberately kept apart from the hard-failure codes 2 to 6 and from 7: the records genuinely verified or restored, so the run should never be read as a clean, complete result nor as a failure. A hard verification or per-record failure always takes precedence over this advisory: the reader only returns 8 once every check that could produce 2 to 6 has already passed.

Code 9 is not in that family at all. It is a hard failure, like 2 to 6, but of the custody artefacts rather than of the archive: no key was recovered, so nothing was read. It is numbered between the two advisories for historical reasons and should not be read as one of them.

Code 10 is kept apart for the same reason and is separate from 8 because the gap is different in kind: an incompleteness marker is a record that landed carrying a placeholder, while an unwritten record did not land at all. Where a restore would qualify for both, 10 is returned, since the absent record is the larger gap. What 10 must never be is 0. A summary line on stderr is not something a recovery script reads, so a restore that skipped records reports it in the exit status rather than letting automation conclude a clean full restore.

Code 12 is the mildest of the three, and the numbers are not a ranking, so read them by what they say rather than by size. Code 10 says records are in the archive and not on your disk. Code 8 says records landed carrying a placeholder instead of the source’s data. Code 12 says every byte landed intact and this release of the reader cannot render one of them, which another release can. Both of the others take precedence over it, and any hard verification or per-record failure takes precedence over all three.

Code 12 is 12 rather than a reuse of 8 because the two point in opposite directions. An incompleteness marker is a statement about the data, that the source was only partly available when the backup ran. Code 12 says nothing about the data at all, only about this reader. A script that treated them alike would either chase a backup-time gap that never happened or apply a file it must not apply. The code exists because the alternative was exit 0 alongside guidance to feed the file to sqlite3: an archive written by a newer engine opens, verifies and restores through a reader that cannot read its D1 bodies, and before this code the only signal an operator got was the parse error sqlite3 gave them afterwards.

Code 13, the dangling-segment code

Code Meaning When the reader returns it What to do
13 Every failed record was a segment object this copy does not hold The signed manifests all verified, and every per-record failure was a seg/ object those manifests name and this copy of the archive does not have. Nothing was retrieved for those records, so no authenticated decrypt, no signed hash and no signature failed. The signed receipt counts the distinct absent paths as danglingSegments, and the completeness verdict reads incomplete Not a tamper finding. Fetch the named objects from another copy of the bucket and re-run, or accept that this copy is incomplete. Retrying against the same copy will keep failing, because the read was answered and the answer was that the object is not there

Code 13 ends a conflation that pointed a recoverer the wrong way. An absent segment and a failed authentication tag both used to exit 2, and worse, both printed the same sentence: completeness=UNVERIFIED (1 of 1 record(s) failed integrity). Driven against a real signed archive, deleting one seg/ object and flipping a byte inside it produced byte-identical verdict lines. The two remedies are opposite. An absent segment means fetch the object from another copy. A failed tag means the bytes were altered and this copy must not be trusted. Being told your data is corrupt when it is merely absent points you away from the replica that would have restored you, which is the more damaging of the two mistakes.

The receipt already drew this line, since danglingSegments reads 1 against an absent segment and 0 against a flipped byte. The exit status and the printed line now agree with it, so the receipt is no longer the only honest surface.

Precedence, because the numbers are not a ranking. Any per-record failure that did fail a check wins: a run with one absent segment and one failed tag exits 2, or 4 for a plaintext mismatch, never 13. A signature or structural verdict on the run itself wins too. Against the three advisories 13 wins, because 8, 10 and 12 all describe records that are in the archive and this describes records that are not.

Code 13 is not a reuse of 3, and not a reuse of 11. Code 3 is the structural count check, raised before a single segment is fetched, and it says the manifests do not add up; 13 says the manifests add up exactly and the bucket is missing an object they name, which is a finding about the destination rather than about the manifest. Code 11 means the destination refused the request or could not be reached before any bytes arrived, so nothing at all is known and a retry against the same place may work; with 13 the read was answered.

Running downpipe with no command at all prints usage and returns 6, the same as any other usage error: a bad flag, an unparseable run id, a missing source, or a missing or unreadable identity, signer or receipt-signer key file. Code 2 is a verdict about an archive, and a run that was given no command has opened nothing to have a verdict about. A partial restore is never presented as a success: see code 1 above for how an uncoded per-record failure is the one case that falls back to a flat 1 rather than surfacing its true class.

The salvage overrides

Three flags let a recoverer proceed past a failing verdict on purpose. They are narrow by design, they never widen each other, and the receipt always records the true outcome rather than presenting an overridden run as clean. Two of them acknowledge different halves of exit 5, so read the stderr line before you choose: it names the acknowledgement that fits what the reader found, and the other one will refuse the command a second time.

--allow-stale acknowledges a run’s age, and only its age

--allow-stale is the acknowledgement for a run the RUNLOG reports as old. It applies when the RUNLOG verified against your --signer, is internally consistent, and says either that this run is not the latest for its downpipe or that its maximum index is below your --min-runlog-index pin. Restoring a superseded run on purpose, for a point-in-time recovery, is exactly what it is for.

It does not waive the RUNLOG’s signature, presence or self-consistency. So it covers two of the four causes listed for code 5 above, not all four: if the reader could not check freshness at all, or checked it and found the log contradicts itself, --allow-stale refuses the command again and the stderr line says so. It also overrides nothing outside code 5: it does not suppress a signature, integrity, completeness or plaintext failure, so a run that is both stale and tampered still fails on the tamper verdict, leaving codes 2, 3 and 4 untouched.

--allow-unverified-runlog acknowledges an untrustworthy RUNLOG

--allow-unverified-runlog is the acknowledgement for the other half of code 5, where the problem is the RUNLOG itself rather than the run’s age. It covers a RUNLOG that is absent, unreadable, unparseable or empty, one that does not carry this run, one that fails to verify against your --signer or disagrees with the signed root manifest, and one that verified and is internally contradictory, such as a duplicated index or a forked chain.

It is a stronger acknowledgement than --allow-stale, and a deliberately uncomfortable one, because nothing is then established about this run’s recency: a bucket someone has rolled back or replaced looks exactly like this. Before you use it, fetch the RUNLOG and its detached signature from a copy you trust and re-run, or compare the log you have against one you trust. Reach for it only when you have satisfied yourself the archive is the one you meant to read.

--allow-unverified is the last-resort salvage path

--allow-unverified overrides a signature or recovery-bundle failure so you can still pull the physically present, authenticated bytes back when a key needed to verify is missing or a signature does not check out. This keeps a key-management slip from becoming total data loss, but it is not verified restore: the bytes are decrypted and authenticated by the envelope, yet the run is not proven whole or genuine. Treat it as a salvage path, never as a routine restore. When you use it, the reader records the true unverified outcome in the receipt rather than dressing it up as verified.

--allow-unverified implies both of the freshness acknowledgements above, so it also clears code 5 in either of its halves. An operator who will proceed on an unsigned root manifest is not being protected by a separate question about the RUNLOG. The narrower flags exist so that acknowledging an old run does not silently also accept an unverifiable one.

An allow-unverified recovery does not prove the run

--allow-unverified recovers bytes; it does not prove them. The run is not shown as genuine, the receipt says so plainly, and you should follow up on why verification failed. Use it only when the alternative is losing the data.

The recovery-bundle binding

--check-bundle is an opt-in extra check that binds the in-bucket recovery documents (the versioned FORMAT.md pointer and RECOVER.md) to their signed SHA384SUMS, so a recoverer who relies on the bundled instructions can confirm the instructions were not altered. A bundle-check failure is fatal: it produces exit 2 unless --allow-unverified downgrades it to a warning. That keeps the verdict honest, because trusting altered recovery instructions is exactly the failure the binding exists to catch.

What the receipt records

A signed, machine-readable receipt records the same outcome in detail. Write one with --receipt <path> (a path of - writes it to stderr so stdout stays clean for a data sink), and sign it with --receipt-signer <file> if you hold the session signer private key. The receipt carries the exit code, the signature and completeness verdicts, the recovery-bundle result, and a valueVerified flag.

valueVerified is true only when the offline binary actually hash-checked the written value against the signed manifest. That is the case for the file, env and discard sinks on a real --apply, where each value is decrypted and its plaintext hash is checked as it is handled (the discard sink verifies the value and then writes nothing). It is false for a dry run, which handles no value, and false for a verify-only run, which writes no value at all. So valueVerified: true on a discard receipt means every record was decrypted and verified even though nothing was written.

Run valueVerified
verify (no value written) false
restore dry run (no --apply) false
restore --apply --sink file true
restore --apply --sink env true
restore --sink discard (always applies) true

Where this fits

For every command and flag that produces these codes, read the CLI command reference, which also owns the deep offline-recovery walkthrough. For the engine-side and in-console restore that the portal drives rather than this binary, read the restore flow. For why a genuine drill uses the break-glass identity and keeps the receipt, read prove recoverability. For the freshness and anti-rollback model the code 5 verdict comes from, read the threat model.

Last updated .