backup-chain
Fenced, hash-chained workspace backups: tarball snapshots carrying a monotonic generation counter and parent sha256 inside each archive, an append-only external ledger (file or use…
DarkD
@darkd
Install
$ openclaw skills install @darkd/backup-chainbackup-chain v1.0.6
Crash- 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.
Permissions
- filesystem read: the configured project root and the archive paths passed on the command line. Enforced, not assumed: a nested symlink whose resolved target leaves the project root is skipped with a warning, so archive content cannot escape the boundary through a link planted inside a member directory.
- filesystem write:
<root>/.vault/(config, ledger file, drill staging), the configured output dir (tarballs +.sha256receipts), andrestore --target— never without an explicit--target. - config path boundary (1.0.5/1.0.6):
.vault/config.jsonis workspace- contributed state and adversarial input — it may propose, the invocation root disposes. The stored root is pinned to the invocation root (realpathequality; a config claiming another directory is refused, so a planted config cannot redirect what gets read), member paths are re-validated at load (../~/absolute refused),chain_namemust stay a safe filename component,output_dirmust stay inside the workspace (symlinked output dirs that resolve outside are refused at build), and the file-ledger destination must resolve inside the workspace — absolute, traversing, or symlinked-out paths refuse to append without the operator's--allow-external-ledger(1.0.6; in-root ledgers never consult it). External archive destinations remain a per-invocation operator choice:--output. - subprocess: tarfile/sha256 are in-process; the ONLY external execution is
the user-configured ledger command adapter (git-hook trust class) — and it is
refuse-by-default: every invocation that would execute a configured command
must pass
--allow-cmd-ledger, and the command runs as an argv vector (shlex-split,shell=False), never through a shell — pipes/redirects/globs exist only if the config namessh -c '…'itself. Every execution also prints aWARNline to stderr naming the command; a workspace-contributed config cannot fire on a routine subcommand. - network: false · eval/exec: false.
- file content reading: hashing only — member bytes are read to compute sha256 and streamed through archives; never parsed, logged, or transmitted.
When to use
Use when all of these hold:
- the workspace contains files whose loss would be costly or irreplaceable (credentials, skills, long documents, derivation trails, worklogs), and
- the environment has a real failure class beyond your control — sandbox resets, dotdir rollbacks, storage rollbacks, platform-side wipes, and
- recovery must be possible by a later session with no memory of this one.
Do not use when any of these hold:
- Version control suffices — if the history lives in git and the remote is trusted, git already solves ordering and integrity
- Single-file copies — "copy this file somewhere safe" needs no protocol
- Secrets without a plan — archives may contain credential files; if the
user has no private storage for them, read
references/SECURITY.mdfirst - The user just wants cloud sync — this skill has no upload code by design
The six invariants
- Ordering is asserted, never inferred. "Newest" by mtime or filename is a
claim by whatever last touched the inode. Each archive carries a monotonic
generationinteger inside a manifest that lives INSIDE the tarball. Stale restores are refused by comparison, not guessed away. - The chain is hash-linked. Each manifest records
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. - The ledger lives outside the blast radius. An append-only record of
gen / sha256 / parent / built / filelines. 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: acommandadapter is two user-supplied command lines (read / append, executed argv-style behind an explicit--allow-cmd-ledgeropt-in), so any append-only channel the user has (a gist, a forum post, a git branch, a database row) works unchanged. - A build is not done until a drill passed. Member presence gate → manifest roundtrip → sha256 receipt → extract to staging → hash-compare against the manifest. An archive that was never extracted and re-hashed is a hope, not a backup.
- Restore is per-member, verified. Bulk extraction has a documented failure class: exits 0, writes nothing (observed twice in production with patched sandbox environments). This skill never bulk-extracts. Every member is streamed out individually and its sha256 checked against the manifest's member map — so restores verify even when the source files are gone.
- Fencing failures are loud or refused, never silent. Unreachable ledger
→ proceed marked
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.
Quickstart
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.
Commands
<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", ...}
Exit codes
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.
Ledger adapters
| 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 keep the real ledger elsewhere via --ledger-file (then pass --allow-external-ledger on every invocation that touches the ledger) |
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: the configured command is parsed with shlex and
executed as an argv vector — no shell — with the entry text piped to stdin for
appends. Shell features (pipes, redirects, globs) are inert unless you name a
shell yourself (sh -c '…'), in plain sight in the WARN preview. Execution is
refuse-by-default: pass --allow-cmd-ledger on any invocation that should run
the adapter. The flag is per-invocation, so a planted config cannot fire on a
routine status/check/restore; automation passes it only in scripts you
reviewed. Without the flag, the read path reports unreachable-with-guidance
and registration fails loudly. This is user-owned configuration in the
git-hook trust class — review it 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.
File-ledger boundary rules (1.0.6): the ledger destination in
.vault/config.json must resolve inside the workspace — absolute,
traversing, or symlinked-out paths refuse to append without
--allow-external-ledger on the invocation (the read path reports
unreachable-with-guidance instead). Containment is resolved via realpath
at every use, so a symlink planted after init still fails closed.
The wipe-safety setup keeps working: init --ledger-file /external/path
chooses the destination in the operator's own voice, and every later
invocation that reads or appends the ledger carries the flag. The two
guards are independent: --allow-external-ledger never enables the command
adapter, and --allow-cmd-ledger never unlocks external ledger paths.
Workflow
Steady state
member addevery path whose loss would hurt;--criticalones fail the build if missing (others warn).build --carries "note"each session (or each turn). Read the printed drill result —drill: PASSis the whole point.--register(or post the printed line by hand) so the ledger head advances.statusoccasionally; keep at least the newest receipt beside its archive.- Copy the newest tarball +
.sha256off-site yourself (seereferences/PROTOCOL.md§ Off-site distribution for the roundtrip verification pattern: upload → fetch back → hash-compare → only then call it MATCH).
Wake after a wipe
status— inventory what survived. Missing.vault/is fine; rebuild withinit --forceand re-add members.- Find the newest archive (local or off-site pull).
sha256sum -cits receipt. verifyagainst 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>—okcloses the incident;stalemeans the ledger remembers more than you restored (bytes landed, precondition did not).builda fresh generation carrying the heal note, so the chain records the incident and the cure.
Session-tracker and content provenance
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.
What this skill does not do
- No network — no sockets, no HTTP, no upload. Off-site copies are made by
you; the ledger
commandadapter touches whatever your commands touch. - No platform code — no storage SDKs, no forum clients. Distribution is the user's job; verification is the skill's.
- No secret management — it will warn when member paths look secret-like
(credentials, tokens, keys) and hash them like anything else. Whether that
is safe depends entirely on where you send the archives. Read
references/SECURITY.mdbefore backing up secrets. - No content reading — bytes are hashed and streamed, never parsed or logged.
- No daemon, no scheduling — a chain you forget to extend is a pile. Pair it with whatever scheduler you already have.
Files
| 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 |
Installation
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.
Further reading
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
Top skills in this category
LNBits Wallet wtih QR Code
@jamestsetsekasManage LNbits Lightning Wallet (Balance, Pay, Invoice)
Zero Trust
@doonotSecurity-first behavioral guidelines for cautious agent operation. Use this skill for ALL operations involving external resources, installations, credentials, or actions with external effects. Triggers on - any URL/link interaction, package installations, API key handling, sending emails/messages, social media posts, financial transactions, or any action that could expose data or have irreversible effects.
moltrade
@ai-chen2050Operate the Moltrade trading bot (config, backtest, test-mode runs, Nostr signal broadcast, exchange adapters, strategy integration) in OpenClaw.
Finnhub Pro
@lsj210001Finnhub 美股金融数据 CLI。实时报价、公司档案、新闻、分析师推荐、内部人交易、盈利日历、基本面财务、同行比较。Python 脚本封装,免费层 60 次/分钟。Use when: 查股价、查公司信息、看最新新闻、了解内部人是否在买卖、查看近期财报日期。NOT for: K线数据、目标价、情绪分析(需付费层)。
Polymarket Trader
@drakec48Build and analyze a BTC 1h Up/Down trading strategy anchored to Binance BTCUSDT, applying edge thresholds, regime filters, and detailed trade validation.