Backup, verify, restore
Snapshots, the restore proof, recovery kits, schedules.
Pushes through the helper already put every commit on the vault. secretree backup is for the rest: every local ref at once, files outside the repository, and the proof that all of it comes back.
Backup
$ secretree backup
generation 000008 pushed: incremental, 4 refs, 1.2 KiB
restore proof OK: generation 000008 rebuilt from the remote (8 generations verified, 3 applied, 4 refs match)
A backup snapshots every ref of the local repository (branches, tags, notes), plus the state archive when configured, into one generation. Then it clones the vault back from the remote into a private directory, verifies every signature and the hash chain, rebuilds the repository, compares every ref with the source, runs git fsck and decrypts the state archive. Only then is the generation recorded as proven.
--full forces a full bundle. When nothing changed, nothing to back up is reported and no generation is written.
Backups work in any repository that ran secretree init, whether or not it uses the helper; init --no-remote sets up a backup-only repository.
Out-of-repository state
Configuration files, databases and ledgers that live next to the code but not in git go into an encrypted archive alongside the bundle. Configure them in .git/secretree/config.json:
"state": {
"include": ["config/config.yaml", "data"],
"exclude": ["*.tmp", "data/cache"],
"pre_hook": "sqlite3 data/state.db \".backup '$SECRETREE_STAGE/data/state.db'\""
}
include lists files or directories relative to the work tree; exclude holds glob patterns. pre_hook runs before archiving with $SECRETREE_STAGE pointing at a staging directory; whatever it writes there is archived too, which is how live SQLite databases are snapshotted consistently. The archive is a deterministic tar compressed with zstd, so an unchanged state produces no new generation.
Secrets should not be in the state archive; they are just more plaintext to protect, not something the tool manages.
Verify
$ secretree verify
vault.json: signed, 2 recipient(s), 2 signer(s)
generation 000001: full 2026-09-18 10:12 1 refs 893 B signed, chained
generation 000002: incremental 2026-09-18 10:15 2 refs 445 B signed, chained
…
generation 000008 rebuilt from the remote: 4 refs, fsck clean
source refs match generation 000008 exactly
verify does the restore proof for the latest (or --generation N) generation from a fresh clone. --quick checks signatures, hashes and the chain without rebuilding. --all additionally hashes every ciphertext of every generation against its manifest, which catches silent corruption of old blobs that a restore would not touch.
Tampering is reported, never worked around:
generation 000005: hash chain broken (a generation was altered or replaced)
generation 000003 bundle: age: failed to decrypt and authenticate payload chunk, file may be corrupted or tampered with
existing chain failed verification, refusing to append
Restore
On any machine, with the recovery kit or the keys already in the key store:
$ secretree restore --vault git@gitlab.com:you/app-vault.git --to ~/code/app --from-recovery-kit app-kit.txt
keys from recovery kit stored in macOS Keychain (service secretree)
repository restored to /Users/you/code/app at generation 000008 (4 refs, HEAD refs/heads/main)
state archive unpacked: 6 files
secretree configured in the restored repository; `secretree backup` continues the same chain
--generation N restores an older snapshot; --no-state skips the state archive; --repo-id picks a repository when the vault holds several. The target directory must not exist. The restored repository is configured for the same vault and chain, so backups continue where they left off.
The recovery kit
init writes it (--kit-out <file>) or prints it. It holds the vault id, the vault URL, the signing key's fingerprint, the age identity and the signing key. Anyone holding it can read every backup, so it belongs on paper in a safe place, not in a cloud drive.
$ secretree kit --print ~/Desktop/app-kit.txt # default printer, or opens the file
$ secretree kit --confirm # status stops warning
A lost kit is fine while any device still holds the keys: run join on a new device and approve it from an old one, or print a new kit from a repository with secretree member request for the public part and the key store for the rest. Losing every device and the kit means the backups are gone; that is what zero knowledge means.
Restore by hand
A vault does not depend on secretree to be readable. With git, age, ssh-keygen and shasum alone: verify vault.json against the fingerprint on the kit, decrypt the manifests, check the chain, decrypt the bundles, git clone --bare the full one and git fetch the incrementals in order, set the refs from the last manifest. The exact commands are in the vault's own README.md and in docs/restore-by-hand.md in the repository; the on-remote format is frozen and documented in docs/vault-format.md.
Schedules
$ secretree schedule --daily 03:00 # launchd agent (macOS) or systemd user timer (Linux)
$ secretree schedule --every 1h
$ secretree schedule --show
$ secretree schedule --remove
The schedule runs secretree backup for this repository. On macOS it uses the login Keychain, so it runs while you are logged in; logs go to ~/Library/Logs/secretree/. On Linux, journalctl --user -u secretree-<repo-id>.