Install
openclaw skills install @darkd/backup-chainFenced, hash-chained workspace backups: tarball snapshots carrying a monotonic generation counter and parent sha256 inside each archive, an append-only external ledger (file or user-supplied command — no platform code), gated builds (member presence, manifest roundtrip, sha256 receipt, restore drill
openclaw skills install @darkd/backup-chainCrash- and wipe-resilient workspace backups built on six invariants learned the hard way. A chain, not a pile of copies: every archive knows its generation and its parent's hash, an append-only ledger outside the failure domain remembers both, and no archive counts as "verified" until a restore drill has passed.
<root>/.vault/ (config, ledger file, drill staging),
the configured output dir (tarballs + .sha256 receipts), and
restore --target — never without an explicit --target.Use when all of these hold:
Do not use when any of these hold:
references/SECURITY.md firstgeneration integer inside a manifest that lives INSIDE the tarball. Stale
restores are refused by comparison, not guessed away.parent_sha256 — the
hash of the previous generation's archive. Parent resolution order: ledger
entry first, local archive with matching embedded manifest second, filename
guessing never.gen / sha256 / parent / built / file lines. If the workspace and its
archives can be destroyed together, the ledger must not be destroyable with
them. The skill ships three adapters — none, file, command — and
knows nothing about any platform: a command adapter is two user-supplied
shell snippets (read / append), so any append-only channel the user has
(a gist, a forum post, a git branch, a database row) works unchanged.SKIP (unverified). Older archive than the ledger head →
refuse (stale handoff). A clearly marked untested solution is more useful
than a falsely verified one; a heroic retry that overwrites newer state is
worse than either.BC="python3 <skill-dir>/scripts/backup_chain.py"
# one-time setup (from the project root)
$BC init --name myproject --ledger file
$BC member add worklog.md --critical
$BC member add skills/
$BC member add download/ # tarballs/receipts inside are auto-excluded
$BC status # review the member list
# every build
$BC build --carries "what changed this turn" --register # --register appends to the ledger
# → prints the ledger line even without --register (for manual posting)
# recovery
$BC verify download/myproject-g12.tar.gz
$BC check download/myproject-g12.tar.gz # pre-restore fencing gate → JSON
$BC restore download/myproject-g12.tar.gz --target /fresh/workspace
$BC recheck 12 # post-apply receipt
The ledger is what makes a wipe survivable: after a total loss, next-gen
still respects the ledger's highest generation, check refuses stale archives
the ledger never registered, and verify can authenticate a pulled archive
against a ledger entry even when nothing local survives.
<BC> = python3 <skill-dir>/scripts/backup_chain.py [--root DIR].
Default root is the current working directory; config lives at <root>/.vault/config.json.
# setup
$BC init [--name NAME] [--output-dir DIR] [--ledger none|file|command]
[--ledger-file PATH] [--read-cmd CMD] [--append-cmd CMD] [--force]
$BC member add PATH [--critical] | member remove PATH | member list
# build and register
$BC next-gen # print next generation: max(local, ledger)+1
$BC build [--carries NOTE] [--label LABEL] [--register] [--output PATH]
$BC ledger-line TARBALL [--append] # print (or append) the ledger entry
# inspect and verify
$BC status # local chain head, ledger head, members
$BC verify TARBALL # sha vs .sha256 receipt + ledger entry
$BC drill TARBALL # standalone restore drill
# fencing and restore
$BC check TARBALL # → {"decision": "restore"|"refuse"|"skip", ...}
$BC restore TARBALL --target DIR [--overwrite] [--dry-run] [--force]
$BC recheck GEN # → {"decision": "ok"|"stale"|"skip", ...}
0 success · 1 failure (gate, verification, drill, or restore mismatch —
the message says which) · 2 refused (fencing refuse, overwrite without
--overwrite, dangerous target). Refusals matter: build && next_thing must
not proceed on the assumption a backup exists.
| adapter | read | append | notes |
|---|---|---|---|
none | — | — | local-chain fencing only; every gate result says so |
file (default) | read .vault/ledger.log | append + fsync | a ledger inside the workspace is NOT wipe-safe — use it for testing, or move it elsewhere via --ledger-file |
command | --read-cmd CMD (stdout = ledger text) | --append-cmd CMD (entry on stdin) | your platform, your credentials, your identity filtering — the skill never learns any of it |
Command-adapter rules: commands are run through the shell exactly as
configured, with the entry text piped to stdin for appends. They are user-owned
configuration in the git-hook trust class — review them before init. If the
channel is shared, filter for identity inside the command (e.g. grep for
your handle), because the ledger parser accepts any line that looks like an
entry.
member add every path whose loss would hurt; --critical ones fail the
build if missing (others warn).build --carries "note" each session (or each turn). Read the printed
drill result — drill: PASS is the whole point.--register (or post the printed line by hand) so the ledger head advances.status occasionally; keep at least the newest receipt beside its archive..sha256 off-site yourself (see
references/PROTOCOL.md § Off-site distribution for the roundtrip
verification pattern: upload → fetch back → hash-compare → only then call
it MATCH).status — inventory what survived. Missing .vault/ is fine; rebuild with
init --force and re-add members.sha256sum -c its receipt.verify against the ledger (authenticates even with an empty workspace).check — refuse on stale handoff is the protocol working; investigate, do
not override.restore --target . — per-member, hash-verified.recheck <gen> — ok closes the incident; stale means the ledger
remembers more than you restored (bytes landed, precondition did not).build a fresh generation carrying the heal note, so the chain records the
incident and the cure.This skill pairs well with a checkpointing skill (e.g. session-tracker) but is
a different layer: the tracker checkpoints task state so work resumes; the
chain guarantees artifact custody so conclusions survive. Custody is not
validity — a sha256 proves a sentence survived a restore, not that it was ever
right. If your workspace records why conclusions were drawn (route files,
premises, falsifiers), put those route files in the member list so they hash
into the same chain as the artifacts they audit. See
references/PROTOCOL.md § Custody vs validity.
command adapter touches whatever your commands touch.references/SECURITY.md before backing up secrets.| Path | Purpose |
|---|---|
<root>/.vault/config.json | Chain configuration (members, ledger, output dir) |
<root>/.vault/backup-chain-dir | Ownership marker |
<root>/.vault/ledger.log | Default file-ledger (append-only) |
<root>/.vault/drill/ | Restore-drill staging (self-excluded from archives) |
<root>/.vault/last_build.json | Receipt of the last build (gen, sha, gates) |
<output-dir>/<name>-g<GEN>[-<label>].tar.gz | Archives (generation in the NAME is a label; the manifest is authoritative) |
<output-dir>/<name>-g<GEN>[-<label>].tar.gz.sha256 | Hash receipts |
cp -r backup-chain/ <your-skills-dir>/backup-chain/
python3 <your-skills-dir>/backup-chain/scripts/backup_chain.py --help
bash <your-skills-dir>/backup-chain/tests/test_backup_chain.sh
Python 3.8+, standard library only, no dependencies, no network.
references/PROTOCOL.md — threat model, the six invariants argued properly,
gate list, fencing state machine, off-site distribution patterns, and the
incident classes behind each rulereferences/SECURITY.md — secrets in archives, ledger-adapter trust, restore
overwrite discipline, audit notesreferences/CHANGELOG.md — version history