Skip to content

Repository Export

A repository export is one repository rendered as canonical NDJSON at an exact repository sequence: a header line, one row per item, and a trailer carrying the record count and a SHA-256 over the row bytes. Nothing is retained — the export is folded on request and streamed straight to you.

Export commands require unrestricted repo:read plus repo:checkpoint-read, the same gate a checkpoint download passes. A narrowed or deny-all read matcher is insufficient, including on a public repository. Any personal access token (PAT) missing repo:checkpoint-read must be reissued.

Terminal window
wh repo export acme/widgets --output widgets.ndjson

--output is required. It refuses to overwrite an existing path, so an interrupted export is never silently discarded. Use --output - to write the stream to stdout; that requires --format pretty, because JSON and JSONL output cannot carry export bytes.

The stream is verified as it arrives: the header must name the repository and mode you asked for, every row must decode canonically, and the closing trailer’s record count and digest must match what was actually read. Nothing is reported until the trailer proves the stream.

On success the command prints the repository, mode, delta base, repository sequence, record count, and digest. With --format json the same fields arrive as one object.

FlagMeaning
--output PATH|-Where to write the export. Required.
--mode headsExport mode. heads is the current version of every item, and the default.
--since-repo-seq NExport only what changed after sequence N.
--at-repo-seq NPin the export to sequence N. Omit it and the server pins the current committed sequence and echoes it in the header.
--asyncPrepare the export server-side and print an export token, then exit.
--async --waitPrepare the export server-side and download it immediately when ready.
--token TOKENRedeem a previously prepared export token and download the result.
--resumeContinue an interrupted export already at --output PATH.

Two exports at the same sequence produce byte-identical rows, so the digest is a stable identity for the export’s content and not merely a transport checksum.

Terminal window
wh repo export acme/widgets --output delta.ndjson --since-repo-seq 120

A delta carries only what changed after the named sequence, with deletions as tombstone rows ("active": false). Fold a delta onto a base by durable identity — active rows upsert, tombstones remove. The SDKs do exactly that for you.

A delta base older than the server’s epoch floor is refused, and the answer is a full export, not a retry.

An export interrupted mid-stream keeps the verified rows it had already written:

Terminal window
wh repo export acme/widgets --output widgets.ndjson --resume

Resume reads the partial file back, replays it through the same verifier, and asks the server to continue after the last whole row at the sequence the original header pinned. There is no sidecar state file: the partial export describes itself.

Because the file supplies the window, --resume refuses --mode, --since-repo-seq, and --at-repo-seq rather than silently ignoring them, and it refuses a file whose header names a different repository. It also needs a real file — stdout cannot be continued.

One synchronous export streams per organization at a time. A second concurrent request is refused, and the CLI names the overflow path.

Use --async to prepare the export server-side without taking the synchronous slot. The command prints an export token and exits immediately:

Terminal window
wh repo export acme/widgets --async

To download the result in the same invocation, add --wait:

Terminal window
wh repo export acme/widgets --output widgets.ndjson --async --wait

To redeem a token printed by an earlier --async run:

Terminal window
wh repo export acme/widgets --output widgets.ndjson --token <TOKEN>

--async and --resume are mutually exclusive: a resume continues an already-pinned stream, not a new job.

Terminal window
wh repo verify widgets.ndjson

verify re-reads a saved export and checks framing, canonicality, record count, sequence agreement, and the content digest. It resolves no profile, authenticates nothing, and contacts no server — it reads only the named local file.

Distinct failures are reported distinctly: a mangled row, a miscounted trailer, a wrong digest, and a stream that never reached its trailer are four different messages.

Verify before you convert an export to another format. Parquet and CSV carry no trailer, so once the NDJSON is transformed the digest can no longer be checked against it.

ConditionThe CLI says
The delta base has expiredThe delta base has expired; run a full export.
Another export is already streaming for the organizationAnother repository export is already streaming for this organization. Retry with --async.
--mode ops is not enabled on the deploymentExport mode “ops” is not enabled on this deployment.
--async is not enabled on the deploymentAsynchronous repository export is not enabled on this deployment.
The queued export job failedThe repository export job failed. Re-running the command starts a new job.
  • --async moves the fold off the request path and prints an export token you can redeem later with --token TOKEN. Add --wait to download immediately in the same invocation.
  • Exports are not retained. There is no listing, no re-download by sequence, and no identity to hand to another caller.
  • There is no cancellation command. Abandon the stream instead.
  • Verification cannot be skipped. The digest is computed over lines that are being decoded anyway.