Back up GitHub and GitLab to storage you lock. Then prove it restores.

Restores the copy.
Compares every ref.
Signs the result.
No human in the loop.

$go install gitdr.io/gitdr/cmd/gitdr@latest
Prove it yourself ›
gitdr backup
$ gitdr backup --config gitdr.yaml
run 20260902T091927Z-a1b2c3d4e5f6, success
acme/api: success
acme/web: success
acme/infra: skipped
manifest: github.com/acme/manifests/20260902T091934Z.manifest.json
$
stdout from a real run. The structured log goes to stderr.

One job, five steps, fail closed.

01

Enumerate

Every repo, over read-only scopes.

02

Verify WORM

Object lock, asked before every write. A change shows up on the next run.

03

Bundle

Mirror clone plus LFS, one bundle, SHA-256.

04

Upload

Create-only, with a retain-until date.

05

Sign

One ed25519 manifest over the run.

Run the drill yourself.

$ gitdr drill --config gitdr.yaml --manifest <key> --output json
{
  "schema": "gitdr.drill/v1",
  "manifestSigned": true,
  "status": "success",
  "eligible": 214,
  "drilled": 214,
  "repos": [
    {
      "slug": "acme/api",
      "status": "success",
      "sourceRefs": 84,
      "bundleRefs": 84,
      "restoredRefs": 20,
      "unreferenced": [
        "refs/merge-requests/1/head",
        "refs/notes/commits"
      ],
      "sourceMatch": true
    }
  ]
}
Real --output json, trimmed: one repository, 2 of 64 unreferenced refs.

84 refs declared, 20 restored, 64 counted apart.
A clone creates no ref for refs/merge-requests/*, so gitdr names them rather than folding them in.

It restores to the history it declares

A fresh clone, against the bundle’s own header.

It declares the history the source had

Against the ref map the manifest signed.

How it compares

Git is content addressed, so a matching commit hash covers its tree, its blobs and its whole ancestry. This is a proof of equality, not a sample.

sourceMatch is absent, not false, when the manifest predates source-ref recording. Missing is not passing.

There is no delete method.
No remove, and no overwrite.Grep the destination packages. A test walks them on every build and fails on a delete call, whatever the method is named.

Who can still delete this?

Holds against everyone

Object Lock in compliance mode.
Or a retention policy you locked.
Not even the account root.

  • Amazon S3
  • Google Cloud Storage
  • Azure Blob
  • Backblaze B2
  • Wasabi

Holds if you trust whoever holds the disks

The lock stops the S3 API.
It does not stop root on the host.

  • MinIO
  • Ceph / RGW

Holds unless an administrator removes it

Bucket locks, not Object Lock.
They stop deletes and overwrites.
gitdr cannot read them.

  • Cloudflare R2

You turn the lock on, not gitdr.
--require-worm stops the run rather than writing to a bucket it could not confirm.
gitdr reads the configuration, not the enforcement.

GitHub and GitLab, read-only.

  • GitHub
  • GitHub Enterprise Server
  • GitLab
  • GitLab self-managed

A GitHub App, or a GitLab group token. Every repository, its LFS objects, and a JSON file of issues, pull and merge requests, releases and labels.

It writes only what changed.

It compares refs, not dates

One git ls-remote against the ref map the last manifest signed. A deleted branch counts as a change.

A duplicate on locked storage is permanent

Compliance mode holds against you as well, so a copy you did not need is one you cannot delete.

Every stored bundle is a complete history.
It restores on its own, chained to nothing.
Nothing is skipped past a third of its retention.

Ship it three ways.

binary

Go or a release download

$ go install gitdr.io/gitdr/cmd/gitdr@latest

Static, Linux amd64 and arm64. Also on the GitHub releases page with checksums and signatures.

container

Hardened image

$ docker run ghcr.io/gitdr-io/gitdr backup --config gitdr.yaml

Wolfi base, non-root, read-only rootfs, no shell. Ships git and git-lfs, signed with cosign.

kubernetes

Helm CronJob

$ helm install gitdr oci://ghcr.io/gitdr-io/charts/gitdr

Runs on a schedule, writes a last-successful-run metric your alerting can watch.