Skip to content
downpipes docs

downpipe exit codes and verification outcomes

The downpipe reader commands finish with a process exit code that says what was proven. This page is the normative contract for those codes, for a break-glass recoverer or a drill script that must react to a verdict, not prose.

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. verify-report returns three of the same codes about a signed report rather than an archive: see the verify-report codes.

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 verdict the verification produced.

CodeMeaningWhen the reader returns itWhat to do
0Verified and completeThe run passed every check: signature, recipient set, key commitment, shard hashes, declared count, Merkle root and RUNLOG freshness, with no per-record problemTrust the result. Keep the receipt as evidence of the drill or restore
2UnverifiedA 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 givenStop and investigate. Confirm you pinned the correct signer public key and supplied the right identity before considering the salvage path below
3Incomplete coverageThe verified coverage is below the run’s declared record count, so the run is not whole even though what is present checks outTreat the run as not whole. Check whether the bucket copy is complete and whether every shard is present, then re-run
4Plaintext hash mismatchA per-record plaintext failed to match its signed hash during restore, which means the archived bytes for that record do not match what was signedTreat the archive as altered for that record. Do not rely on the restored value; investigate the destination bytes
5Freshness or rollback problemA 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 manifestRead 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
6Usage or input errorA 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. From reader 0.3.4, also a root manifest whose envelope names a suite or chunk size other than the pinned downpipe/0.1.0 valuesFix the command. The stderr message names the missing or invalid input. For an envelope refusal it names the field and the pinned value, and the run was not written in the downpipe/0.1.0 format

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

CodeMeaningWhen the reader returns itWhat to do
1An I/O, connectivity or unexpected failureAn error the reader did not tag with one of the other codes: for example inspect could not read the root manifest, or the restore target could not write a recordNot 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 14 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 write the restore target refused. Unlike the hard-failure codes 2 to 6, code 1 is worth a retry once the underlying 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

CodeMeaningWhen the reader returns itWhat to do
11The destination could not be reachedA 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 or --azure-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 backNot a verdict on the archive. Its bytes were never retrieved, so nothing about it is yet known. Check the endpoint, the bucket or container, and the credentials named in the printed message, then retry

Code 11 sits outside the six-code SPEC-normative set, the same way 7 to 10 and 12 to 14 do. It marks a read that retrieved no bytes at all, which code 2 does not cover. Code 2 means bytes were retrieved 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 --azure-endpoint or --azure-container), or an expired AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (or AZURE_STORAGE_KEY / AZURE_STORAGE_SAS_TOKEN), worth a retry once fixed rather than an escalation. The two codes ask for opposite responses: retry once the endpoint or credentials are fixed, or stop because the archive may be compromised.

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 keeps the code its read site assigns, most often 2: the deliberately safer default, since an unclassifiable failure reads as a possible tamper finding, never as a silent invitation to retry.

From reader 0.3.4, when a pass reads every segment (verify --deep, or an applied restore) and the run itself already has a code from 2 to 6, a segment read that fails this way does not replace that code. A signature failure with an unreachable segment exits 2, not 11.

Code 11 is reachable over either network backend, --s3-endpoint and --azure-endpoint. verify, attest, restore, inspect, keys and prune all honour it on the same boundary, and between the two backends that covers all four destinations: R2, an S3-compatible store and Google Cloud Storage over --s3-endpoint, and Azure Blob Storage over --azure-endpoint (see destination providers compared). On Azure the two you are most likely to meet arrive named rather than as a bare status: a wrong, absent or expired credential answers 403 AuthenticationFailed, and a container that is not there answers 404 ContainerNotFound. 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 the code its read site assigns, 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.

CodeMeaningWhen preflight returns itWhat to do
7Preflight failedOne or more deploy-time account checks failed or could not be run with this token, or an explicitly requested --domain could not be verified against an active zoneResolve 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 shows as unknown and still exits 7, because a check that could not run is not a pass. 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

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

CodeMeaningWhen the reader returns itWhat to do
8Verified or restored, but contains incompleteness markersThe 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 dataTreat 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
9Custody integrity: the shares or envelope are wrongA 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 withCheck 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
10Restored, but some records were not writtenThe 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 collisionThose 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
12Restored, but this release cannot render some bodiesThe 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 unchangedNothing 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. Its number sits between two advisories, but it is not 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. An archive written by a newer engine can open, verify and restore through a reader that cannot render its D1 bodies, because the body-format label is versioned independently of the archive. Code 12 tells you this before sqlite3 does.

Code 13, the dangling-segment code

CodeMeaningWhen the reader returns itWhat to do
13Every failed record was a segment object this copy does not holdThe 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 incompleteNot 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 separates an absent segment from a failed authentication tag, which exits 2, and the two print different 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 draws the same line: danglingSegments reads 1 against an absent segment and 0 against a flipped byte, and the exit status and the printed verdict line agree with it.

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.

Code 14, the incomplete-prune code

CodeMeaningWhen the reader returns itWhat to do
14A prune left planned work undoneprune --apply finished its pass, but the destination refused or failed at least one planned delete, or the reader kept a run tree because one of its segments is still there. From reader 0.3.4.Read the REFUSED, FAILED and KEPT lines and the receipt. A refusal can be a lock or a missing delete permission. A failure is worth a retry. Run the prune again once the cause is fixed

Code 14 exists so an automated caller does not read a prune that removed nothing as a success. Reader 0.3.3 exits 0 for a fully refused prune --apply. The code applies to --apply only: a dry run plans and deletes nothing, so it has nothing to leave undone.

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, or about a report for verify-report. A run 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 verify-report codes

From reader 0.3.4, verify-report checks the signature on an assurance report the engine generated, against your signer.pub. It returns 0, 2 or 6, and each code is a statement about the report file you gave it, not about an archive. The numbers keep their meaning from the normative table above. Code 2 is a check that ran and failed, and code 6 is input the command could not check.

CodeMeaningWhen verify-report returns itWhat to do
0The report verifiedBoth signature halves, Ed25519 and ML-DSA-87, verified under the --signer key over the canonical JSON of the report’s kind, generatedAt, period and data. The holder of the matching signer private key signed those four members, and none of them changed after signingCompare the printed signer fingerprint with the one on your recovery sheet, unless you passed that value as --signer-fingerprint
2The report did not verifyThe report has no signature, or a signature member that is not a string. The signature has a scheme tag other than edmldsa1:, is not unpadded base64url, or does not decode to 4,691 bytes. One half or both halves fail under this signer: the report changed after signing, a different signer signed it, or one half of the signature or of the --signer file is damaged. Or the file holds content the engine does not write: a top-level member other than kind, generatedAt, period, data and signature, a missing signed member, a repeated member name, an unpaired UTF-16 surrogate, bytes that are not UTF-8, a number with a fraction or an exponent, -0, or an integer beyond 2^53 - 1Do not rely on the report. An unsigned report is informational only. When one half fails, the message names it: take the report JSON again from the engine, then another copy of signer.pub from your recovery kit. A new fetch is a new report: rely on its figures, not on those of the report that failed.
6The input is not a report JSON, or the command is wrongThe report PDF, a UTF-16 file with a byte order mark, an empty file, a JSON syntax error, content after the JSON value, a value that is not an object, or an object with neither kind nor signature. Also JSON nested deeper than 256 levels, a file over 64 MiB, a missing or unreadable --report or --signer, a --signer file that is not a signer public key, an unknown flag or a positional argument, a malformed --signer-fingerprint, or a --signer file whose fingerprint is not that valueFix the input and run the command again. For the PDF, take the report JSON instead, because the PDF carries no signature the command can check

A code 0 covers only the JSON you passed. Each request to the engine generates a new report, so a PDF and a JSON fetched after it are two separate reports, and verifying the JSON says nothing about the PDF. Code 0 also does not check the report’s figures against your account again: it shows who signed them and that they did not change. For the report kinds and the auditor’s procedure, read signed reports.

The salvage overrides

Three flags let a recoverer proceed past a failing verdict on purpose. They are narrow by design, never widen each other, and the receipt always records the outcome: an overridden run is never presented 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 trusted copy and re-run, or compare your log 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 to still return the physically present, authenticated bytes when a verification key is missing or a signature fails. 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 receipt records the mode allow-unverified and the signature result.

--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 records the override, 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. Trusting altered recovery instructions is 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. From reader 0.3.4, when records fail, the receipt’s exit code is the code the process exits with, for restore and for verify --deep. This code can be the uncoded 1, for example when the restore target could not write a record. The receipt does not record the advisory codes 8, 10 and 12.

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 without --deep, which reads no value at all. A verify --deep sets it true only when every record decrypted and hash-checked. So valueVerified: true on a discard receipt means every record was decrypted and verified even though nothing was written.

RunvalueVerified
verify without --deep (no value read)false
verify --deeptrue only when every record verified
restore dry run (no --apply)false
restore --apply --sink filetrue
restore --apply --sink envtrue
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 .