Chapter III · Self-hosted infrastructure

Backup & Recovery

Tested on
OpenClaw 2026.10.4, brigade 0.28.0, restic with rclone, Ubuntu 24.04. Three encrypted restic repositories (a local SMB NAS, a cloud repo on Google Drive, and an add-only photo archive), a Proxmox Backup Server for the hypervisor guests, and a separate two-minute canonical sync for a KeePass database.
Last updated
2026-10-17

How to protect your OpenClaw workspace, configuration, and memory from data loss. Encrypted backups, restore testing, and disaster recovery planning.

What Needs Backup

Your OpenClaw instance has three categories of data, each with different backup priorities:

Critical (Lose This, Start Over)

DataLocationWhy Critical
OpenClaw config~/.openclaw/openclaw.jsonAgent definitions, model assignments, channel tokens, all settings
OpenClaw state databases~/.openclaw/ (SQLite files)Sessions, auth profiles, cron store. Back up a consistent copy, not the live file (see Consistent state staging)
Workspace files~/.openclaw/workspace/SOUL.md, AGENTS.md, USER.md, TOOLS.md, MEMORY.md, all personality and operational files
Knowledge cards~/.openclaw/workspace/memory/cards/Curated long-term memory, hard to reconstruct
Skills~/.openclaw/workspace/skills/Custom skills you’ve written or configured
SSH keys~/.ssh/Access to remote machines
Secrets-render config~/.config/ entries for your secrets toolingRecovery configuration only. Hardware-sealed credentials may need re-enrollment on new hardware

Important (Painful to Lose)

DataLocationWhy Important
Daily memory logs~/.openclaw/workspace/memory/Session history, can be reconstructed but time-consuming
Rules~/.openclaw/workspace/rules/Behavioral rules, corrections, learned patterns
Hooks~/.openclaw/hooks/Custom hook scripts
Brigade runbooks and receipts~/.openclaw/workspace/.brigade/The schedule definitions below and their run history
Systemd user units~/.config/systemd/user/Every timer and browser lane on the host
OAuth state~/.codex/, ~/.claude/Losing these means re-authenticating every subscription after a restore

Nice to Have (Replaceable)

DataLocationNotes
Project repos~/repos/Stored on GitHub, can be re-cloned
Node modulesnode_modules/Reinstallable via npm
Build artifactsdist/, .next/, etc.Regenerated from source
Ollama models~/.ollama/Re-downloadable
Browser caches~/.openclaw/browser/**/CacheVolatile and regenerated. Cookies and sessions are still captured

Backup Strategy

Why Restic

The tar + gpg pattern in the original version of this guide works but has two weaknesses: every backup is a full archive (no deduplication), and restoration requires the entire archive to be intact. I moved to restic, which deduplicates across snapshots, encrypts at rest by default, and lets you mount old snapshots as filesystems for partial restores.

The Tiers

The same backup paths are written into independent encrypted restic repositories, plus two other tiers that cover different data:

TierWhereCadenceWhat it holds
NAS repoSMB share, /mnt/nas/backups/openclaw-restictwice dailyOpenClaw config, workspace, state, scripts, repos. Tag nightly-nas
Cloud repoGoogle Drive via rclone, rclone:gdrive:Backup/openclaw-resticweeklySame paths, independent repo and retention. Tag nightly
Photo archiveGoogle Drive via rclone, rclone:gdrive:Backup/media-resticdailyFamily photos, phone backups, and Immich originals plus database dumps. Add-only, never pruned
Hypervisor guestsProxmox Backup Server on local fast storagedailySnapshot-mode backups of the production containers and VMs
KeePass vaultNAS plus Drive, single fileevery 2 minutesNewest-wins file sync, not snapshot history

The NAS and cloud repos are two separate repos, not one repo copied to the other. restic runs a fresh, deduplicated backup into each. A corrupt NAS repo can’t propagate to Drive, and the two have independent retention.

Why the cloud copy is weekly, not twice-daily: I originally ran both destinations on the same twice-daily schedule. Daily restic-over-rclone writes exhausted the Google Drive API quota, and because the script used set -e, the failing Drive phase aborted the whole run and took the NAS backup down with it. I split the cadence and dropped set -e so a Drive hiccup can never block the local backup. The root cause turned out to be rclone’s shared client ID, which has a small per-minute query quota. A private OAuth client ID is the real fix. Pacing is the workaround. See Drive Quota and Over-Syncing.

How the Schedules Run

Since mid-2026 the restic jobs are not bare crontab lines. Each one is a Brigade runbook: a small JSON file with an allowed_commands list and one step that runs the script with a timeout. Running it through brigade runbook run --approved writes a reviewed receipt per run under .brigade/runbooks/runs/, so a scheduled backup that fails leaves evidence instead of a line in a log nobody reads.

The shape of a runbook (names and paths are examples):

{
  "id": "nas-restic-backup",
  "description": "Twice-daily NAS restic backup.",
  "approved": true,
  "allowed_commands": ["bash"],
  "steps": [
    {
      "id": "backup-nas",
      "run": "bash ~/.openclaw/workspace/scripts/backup-restic.sh nas",
      "timeout_seconds": 7200
    }
  ]
}

Who triggers which runbook:

RunbookTriggerStep timeout
NAS backupOpenClaw cron job (kind: command), twice daily, announces the result to chat2 hours
Cloud backupOpenClaw cron job (kind: command), weekly on Sunday8 hours
Photo archive backupsystemd user timer, daily around 07:30 with a 10 minute random delay23 hours
Freshness checksystemd user timer, daily 09:0015 minutes
Integrity verifysystemd user timer, Mondays 16:007 hours
Restore rehearsalsystemd user timer, first Sunday of the month 06:002 hours
Small app-level backups (search index DB, agent session files, a pull of a second host’s OpenClaw state)crontab lines of the form brigade runbook run --target <workspace> --approved <runbook>.jsonper runbook

The cloud runbook points at a thin retry wrapper instead of the backup script directly. It reruns the backup only when the shared lock helper reports that another maintenance job held the lock (exit 75), up to three attempts with a ten minute backoff. Any other exit code is returned on the first occurrence.

Two reasons the schedule is split across cron, systemd, and OpenClaw cron: the OpenClaw cron jobs give a chat announcement of the result for free, and the systemd timers give Persistent=true catch-up after a reboot. See cron patterns for choosing a scheduler.

The Backup Script

One script, backup-restic.sh [nas|gdrive|both|media] [--no-prune], owns every restic write. It has no set -e: each repo runs in a function that returns non-zero on failure, and the script reports a per-repo exit summary. An abridged skeleton:

#!/usr/bin/env bash
# backup-restic.sh [nas|gdrive|both|media] [--no-prune]
set -uo pipefail                       # NOT set -e: a Drive failure must not abort the NAS phase

# 1. One shared lock for every restic job on the host (details below)
source "$(dirname "$0")/restic-shared-lock.sh"
restic_shared_lock_acquire || exit $?

# 2. Scratch space off the root volume
export TMPDIR="$HOME/.cache/restic-tmp"
export RCLONE_TEMP_DIR="$TMPDIR"
mkdir -p "$TMPDIR"

# 3. Conservative rclone backend for Google Drive
export RCLONE_TRANSFERS=1 RCLONE_CHECKERS=2 RCLONE_TPSLIMIT=2
export RCLONE_DRIVE_PACER_MIN_SLEEP=500ms RCLONE_RETRIES=8 RCLONE_LOW_LEVEL_RETRIES=20

run_backup_phase() {                   # label, repo, tag
  export RESTIC_REPOSITORY="$2"
  restic unlock                        # removes only locks whose owner process is gone
  restic snapshots --json &>/dev/null || restic init || return 1
  restic backup --tag "$3" "${EXCLUDES[@]}" "${PATHS[@]}" || return 1
  if [ "$PRUNE" -eq 1 ]; then
    restic forget --group-by host,tags --keep-daily 7 --keep-weekly 4 --keep-monthly 3 --prune \
      || echo "WARN: prune failed (backup ok)"
  fi
}

The details that earned their place:

Consistent State Staging

OpenClaw keeps live state in SQLite databases. Copying a live SQLite file with a file-level backup can capture a torn page. The script therefore stages a recovery set before restic runs: a consistent snapshot of ~/.openclaw taken with a snapshot helper (SQLite online backup for each database), the current systemd user units, and a small manifest recording which OpenClaw commit the gateway runs. The live database files are excluded from the restic inputs, the staged copies are included, and the staging directory is deleted after restic finishes. If staging fails, the backup aborts before restic starts rather than writing a snapshot that cannot restore the databases.

The Shared Lock

Every local restic entrypoint (backup, freshness check, verify, rehearsal) sources one helper and takes the same flock on a file under /run/user/<uid>/. It waits up to ten minutes (RESTIC_LOCK_WAIT_SECONDS), and if the lock is still held it exits 75 without starting any restic command. This is what stops a Monday verify run from colliding with a long cloud backup. The helper intentionally does not clear repository locks. Recovering from a stale repository lock stays an operator action.

Drive Quota and Over-Syncing

The single most important lesson from running this in production: Google Drive throttles you when too many rclone jobs hit it at once, and over-frequent syncing makes it worse, not better. Three defenses are in place.

1. Throttle the rclone backend. The RCLONE_* env vars force one transfer at a time, a transactions-per-second cap, a pacer, and deep retry counts. Restic-over-rclone with default parallelism bursts dozens of API calls and trips Drive’s per-user rate limit, after which every job spins in a quota-retry loop and nothing finishes.

2. Don’t run two rclone jobs against Drive simultaneously. An Obsidian vault bisync (rclone to Drive) runs every two minutes. If a restic Drive phase overlaps it, both jobs fight for the same quota. The backup script pauses the Obsidian sync timer for the duration of any Drive phase (cloud and photo archive) and resumes it on exit:

trap 'systemctl --user start obsidian-sync.timer 2>/dev/null || true' EXIT
systemctl --user stop obsidian-sync.timer obsidian-sync.service 2>/dev/null || true
# ... run the Drive phase ...

3. Serialize with the shared lock so two restic jobs never overlap in the first place.

The general rule: serialize anything that talks to Drive. More frequent syncing does not give you a fresher off-site copy, it gives you a rate-limited one. A shared rclone client ID makes this worse because the quota is shared with every other user of that client. Create your own OAuth client ID for rclone if you can.

The Encrypted Photo Archive

Family photos and phone backups are the data I cannot regenerate, so they get their own repo with different rules:

Proxmox Backup Server

The hypervisor guests are covered by Proxmox Backup Server, not restic. A scheduled vzdump job runs daily at 02:00 in snapshot mode with zstd compression and covers the production containers and VMs. The PBS datastore lives on local fast storage (its own logical volume), at roughly 70% full when I last looked.

Two design points:

Set Up the Passphrase

openssl rand -base64 32 > ~/.openclaw/.restic-password
chmod 600 ~/.openclaw/.restic-password

Store this passphrase somewhere outside your machine (password manager, printed copy in a safe). If you lose it, every restic repository becomes unreadable. Because the passphrase file lives under ~/.openclaw, it is also inside the backup, which only helps if you can already read a repo, so the off-machine copy is not optional.

Initialize the Repositories (One Time)

The backup script runs restic init itself when a repo is missing, but doing it by hand once lets you see errors:

export RESTIC_PASSWORD_FILE=~/.openclaw/.restic-password
restic -r /mnt/nas/backups/openclaw-restic init
restic -r rclone:gdrive:Backup/openclaw-restic init
restic -r rclone:gdrive:Backup/media-restic init

Schedule the Backups

Write one runbook per job, then trigger it from the scheduler that fits. A systemd user timer for a daily job looks like this:

# media-restic-backup.timer
[Timer]
OnCalendar=*-*-* 07:30:00
RandomizedDelaySec=10m
Persistent=true

# media-restic-backup.service (Type=oneshot)
[Service]
ExecStart=%h/.local/bin/brigade runbook run --target %h/.openclaw/workspace --approved %h/.openclaw/workspace/.brigade/runbooks/media-restic-backup.json
TimeoutStartSec=24h
Nice=10

Set TimeoutStartSec above the runbook step timeout, otherwise systemd kills a backup that the runbook still considers healthy. For the OpenClaw cron route, create a command-kind job that runs the same brigade runbook run line and announces the result. See OpenClaw cron.

Monitoring the Backups

Three independent checks sit on top of the backup jobs, each deliberately simple:

CheckCadenceWhat it proves
Freshness checkdaily 09:00The newest snapshot in the NAS and cloud repos is younger than a per-repo threshold (24 hours for NAS, 8 days for the weekly cloud repo). Posts the result to an n8n webhook that alerts on staleness. The webhook secret comes from a 0600 file and reaches curl through a private config file, never argv or the environment
Integrity verifyMondays 16:00restic check --read-data-subset=5% on the NAS, cloud, and photo repos, each with its own timeout. Writes the result to the Brigade summaries and a small status JSON for a dashboard. A run that is interrupted marks the check failed, so an old ok cannot linger
Restore rehearsalfirst Sunday of the month 06:00Actually restores something. See below

A fourth, softer check: a daily chat brief reads the Brigade summaries and the n8n execution list (read-only) and summarizes backup health. That job is forbidden from running restic, rclone, or any backup script itself.

The freshness check does not cover the photo archive. The integrity verify and the rehearsal do, and the daily photo timer leaves a Brigade receipt.

Restore Rehearsal

A backup you have never restored from is a backup that doesn’t exist. The monthly rehearsal script (openclaw-restic-restore-rehearsal.sh [nas|gdrive|media|all]) automates the smallest meaningful restore:

  1. For the NAS and cloud repos, restore the staged recovery state from the newest snapshot and check that the snapshot manifest, the staged state database, the source-identity manifest, and the gateway unit file are present. Then run PRAGMA integrity_check on every SQLite database the manifest lists. It deliberately does not start a gateway: the scope is file and database integrity.
  2. Restore one stable sample file from the newest snapshot and compare it byte for byte with restic dump of the same path.
  3. For the photo archive, do the same sample restore and comparison with a known photo.
  4. Record the result and timestamp into the Brigade summaries, so brigade work backup doctor can flag a rehearsal that has not run recently.

The script takes the shared lock, so it waits behind a running backup. Treat a failed rehearsal as a real incident. It is the only job that proves the snapshots restore into something usable.

Backup Destinations

Local NAS (Primary)

Fast restores and large backups. I use an SMB NAS mounted at /mnt/nas via fstab automount with guest access. The NAS is the household storage tier. The OpenClaw backup pool sits alongside unrelated data, so treat it as shared infrastructure.

# fstab entry (automount on demand)
//<NAS_HOST>/share /mnt/nas cifs guest,uid=1000,gid=1000,nofail,x-systemd.automount,x-systemd.idle-timeout=300 0 0

Write policy is a convention, not a mount flag. The mount is read-write, because the backup jobs must write to the backup directory. Nothing in the kernel stops an agent or a typo from deleting the photo archive on the same share. The guards are procedural:

If you want a real guard, split the share: a read-only mount for general use and a separate authenticated writable mount that only the backup service uses. I have not done that split yet. See NAS and Network Storage Mounts for the mount options.

Cloud Storage (Off-Site)

Google Drive via rclone. Restic handles encryption, the rclone transport is just the storage tier.

rclone config  # one-time: authenticate against Google Drive (use your own client ID)

The 3-2-1 Rule

For a homelab OpenClaw setup, “local disk + NAS + cloud” covers all three. The photo archive meets it through the NAS original, the add-only Drive repo, and the phone or Immich copy it was ingested from.

KeePass Canonical Sync

The restic repos are snapshot history. A password database is different: it changes constantly, you edit it from more than one place, and you need a single canonical copy that’s always current, not a snapshot from this morning. I keep one KeePass .kdbx and sync it between the NAS and Google Drive on a short timer with newer-mtime-wins resolution.

This is not part of the restic backup. The restic script comments the vault out of its path list on purpose so the vault is never read or copied through that path. It is a separate, lightweight, bidirectional single-file sync whose job is to keep the two copies identical so neither location is ever stale.

Two user timers are live:

TimerCadenceJob
keepass-sync.timerevery 2 minutes (OnBootSec=45, OnUnitActiveSec=2min)Newer-mtime-wins sync of the vault between NAS and Drive
op-keepass-mirror.timerdaily 05:15 with a 5 minute random delay, Persistent=trueMirrors items from the password manager’s read-only service account into the vault so the KeePass file is a complete offline copy

The mirror writes entries under a dedicated group with a title prefix so they cannot collide with runtime entry titles. It matches by the source item id, updates an entry when the source changes, and never deletes one, so the vault keeps an item after it leaves the password manager. It takes a timestamped backup of the vault before every save (keeping the newest 20) and holds the same lock as the sync script while writing, so a mirror run cannot race a sync tick.

#!/bin/bash
# keepass-sync.sh - bidirectional, newer-mtime-wins (5s drift tolerance)
set -uo pipefail
NAS_FILE="/mnt/nas/share/vault.kdbx"
GDRIVE_REMOTE="gdrive:vault/vault.kdbx"
LOG="$HOME/.openclaw/workspace/logs/keepass-sync-$(date +%Y%m%d).log"
TOLERANCE=5

# flock so overlapping timer ticks exit cleanly instead of racing the file
exec 200>/tmp/keepass-sync.lock
flock -n 200 || exit 0

[ -f "$NAS_FILE" ] || { echo "$(date -Is) ERROR: NAS file missing" >>"$LOG"; exit 1; }
NAS_MTIME=$(stat -c %Y "$NAS_FILE")

GDRIVE_JSON=$(rclone lsjson "$GDRIVE_REMOTE" 2>/dev/null || echo "[]")
GDRIVE_MTIME_ISO=$(echo "$GDRIVE_JSON" | python3 -c "import json,sys;d=json.load(sys.stdin);print(d[0]['ModTime'] if d else '')")

if [ -z "$GDRIVE_MTIME_ISO" ]; then              # Drive copy missing -> seed from NAS
  rclone copyto "$NAS_FILE" "$GDRIVE_REMOTE"; exit $?
fi

DIFF=$(( NAS_MTIME - $(date -d "$GDRIVE_MTIME_ISO" +%s) ))
if   [ "$DIFF" -gt  "$TOLERANCE" ]; then rclone copyto "$NAS_FILE" "$GDRIVE_REMOTE"   # NAS newer -> push
elif [ "$DIFF" -lt "-$TOLERANCE" ]; then rclone copyto "$GDRIVE_REMOTE" "$NAS_FILE"   # Drive newer -> pull
fi                                               # within tolerance -> no transfer

Design notes that matter:

Restore Procedure

Full Restore Steps

export RESTIC_PASSWORD_FILE=~/.openclaw/.restic-password   # restored from your off-machine copy

# 1. List available snapshots (from either destination)
restic -r /mnt/nas/backups/openclaw-restic snapshots
# Pick a snapshot ID to restore from

# 2. Restore to a temp location for inspection
restic -r /mnt/nas/backups/openclaw-restic restore <SNAPSHOT_ID> --target /tmp/restore-test

# 3. Verify contents
ls -la /tmp/restore-test/home/*/.openclaw/
jq . /tmp/restore-test/home/*/.openclaw/openclaw.json > /dev/null && echo "OK config parses"

# 4. Check critical files exist
for f in SOUL.md AGENTS.md MEMORY.md USER.md TOOLS.md; do
  [ -f /tmp/restore-test/home/*/.openclaw/workspace/$f ] && echo "OK $f" || echo "MISSING $f"
done

# 5. Count knowledge cards
CARDS=$(ls /tmp/restore-test/home/*/.openclaw/workspace/memory/cards/*.md 2>/dev/null | wc -l)
echo "Knowledge cards: $CARDS"

# 6. Clean up test
rm -rf /tmp/restore-test

For the databases, restore from the staged recovery copy rather than the live file path: the staged snapshot lives under the staging directory path inside the snapshot, and its manifest lists every database. Run sqlite3 <db> 'PRAGMA integrity_check;' on each before pointing a gateway at it.

Mount a Snapshot Without Restoring

One restic advantage: browse old snapshots like a filesystem without pulling anything.

mkdir -p /tmp/snap-mount
restic -r /mnt/nas/backups/openclaw-restic mount /tmp/snap-mount &
ls /tmp/snap-mount/snapshots/
# Navigate and read any historical file, then:
fusermount -u /tmp/snap-mount

Restore to a New Machine

OpenClaw runs from a release worktree now, not a global npm install, so a rebuild is: restore, rebuild the recorded commit, point the gateway at it. See Upgrade Hygiene for the worktree layout.

# 1. Install the OS, Node (24.x), git, pnpm, restic, rclone
sudo apt install restic -y

# 2. Restore the passphrase from your off-machine copy, then restore the latest snapshot
export RESTIC_PASSWORD_FILE=~/.openclaw/.restic-password
restic -r /path/to/repo restore latest --target /

# 3. Rebuild OpenClaw from the commit recorded in the restored source manifest
#    (the staged manifest lists the origin URL, commit, and rebuild commands)

# 4. Restore the staged systemd user units, reload systemd, start the gateway
systemctl --user daemon-reload
systemctl --user start openclaw-gateway

# 5. Verify
openclaw --version
jq . ~/.openclaw/openclaw.json > /dev/null && echo "OK config parses"

# 6. Pull local models again and re-authenticate hardware-sealed credentials
ollama pull qwen3-embedding:8b

Hardware-sealed credentials (for example a TPM-bound secret) do not transfer to new hardware. Plan to re-enroll them from the password manager.

Recovery Time Objective

With a good backup and documented procedure, you should be able to rebuild from scratch on new hardware in a couple of hours, most of it build and download time:

StepTime
Install OS + Node.js15 min
Restore backup10 min
Rebuild OpenClaw from the recorded commit10 min
Install Ollama + models10 min
Re-enroll sealed credentials, verify channels20 min
Test agent responses10 min
Total~75 min

I have rehearsed the restore of state and a sample file monthly, but not a full bare-metal rebuild on spare hardware, so treat the table as an estimate.

Database Backup Warning

If your agent uses SQLite databases (code search index, analytics, OpenClaw state), be aware:

sqlite3 /path/to/database.db ".backup /path/to/backups/database-$(date +%Y-%m-%d).db"

Verification

export RESTIC_PASSWORD_FILE=~/.openclaw/.restic-password

echo "=== Latest Snapshot (NAS, expect < 16h old) ==="
restic -r /mnt/nas/backups/openclaw-restic snapshots --latest 1 2>/dev/null || echo "NAS repo unavailable"

echo ""
echo "=== Latest Snapshot (Google Drive, expect < 8 days old) ==="
restic -r rclone:gdrive:Backup/openclaw-restic snapshots --latest 1 2>/dev/null || echo "rclone repo unavailable"

echo ""
echo "=== Latest Snapshot (photo archive, expect < 2 days old) ==="
restic -r rclone:gdrive:Backup/media-restic snapshots --latest 1 2>/dev/null || echo "photo repo unavailable"

echo ""
echo "=== Passphrase File ==="
[ -f ~/.openclaw/.restic-password ] && echo "OK passphrase file exists" || echo "passphrase file missing"

echo ""
echo "=== Backup, verify, and rehearsal timers ==="
systemctl --user list-timers --all --no-pager | grep -E 'restic|keepass'

echo ""
echo "=== Brigade backup summaries ==="
jq -c '{destination_label, latest_snapshot_at, latest_check_result, latest_restore_rehearsal_result}' \
  ~/.openclaw/workspace/.brigade/backups/*-summary.json

echo ""
echo "=== OpenClaw cron backup jobs ==="
openclaw cron list --json | jq -r '.jobs[] | select(.name | test("backup"; "i")) | [.name, .enabled, .state.lastStatus] | @tsv'

Expected output: the three snapshot lines show timestamps inside the stated windows, the timer list shows keepass-sync, op-keepass-mirror, restic-freshness-check, openclaw-restic-verify, openclaw-restic-restore-rehearsal, and the photo archive timer with future NEXT times, each Brigade summary shows ok for check and rehearsal, and each backup cron job reports a last status of ok. A gdrive snapshot older than eight days or a job in error is the signal to read that run’s receipt under .brigade/runbooks/runs/.

Gotchas

  1. Test your restores. Seriously. Encrypt a backup, delete it from the original location (in a safe environment), and restore it. If you can’t restore, you don’t have a backup. The monthly rehearsal job exists to make this routine.

  2. Store the passphrase separately. If your backup passphrase is only on the same disk as your backups, a disk failure loses both. Put it in a password manager or print it.

  3. API keys in backups. Your encrypted backup contains API keys, tokens, and SSH keys. Treat the repository itself as sensitive. Don’t upload unencrypted backups to public cloud storage.

  4. Ollama models aren’t in the backup. They’re large (GBs) and re-downloadable. Just re-pull after restore.

  5. Schedules live in more than one place. Cron jobs live in OpenClaw’s state, timers live in ~/.config/systemd/user/, and runbooks live in .brigade/. The backup includes all three, but on a rebuild you must restore the OpenClaw cron store and enable the timers again. openclaw cron list --json > cron-export.json is a cheap periodic export.

  6. forget --prune is destructive by design. The retention flags delete snapshots that don’t match. If you typo the keep counts, you lose snapshots. Dry-run the first few prune cycles with --dry-run, and use --no-prune when changing the source set. Never prune the add-only photo archive.

  7. Back up OAuth state files, not just OpenClaw config. ~/.codex/auth.json and ~/.claude/ aren’t in ~/.openclaw/, but losing them means re-authenticating every subscription after a restore.

  8. The NAS is writable, so the policy is only as good as your habits. I do not enforce read-only with a mount option, because the backup jobs write there. Keep the add-only off-site photo repo as the real protection, and confirm backup evidence before deleting anything from the NAS.

  9. Never use set -e across two backend phases. With set -e, a failed Google Drive phase aborts the script before the NAS backup runs. Use set -uo pipefail, run each repo in a function that returns non-zero on failure, and report a per-repo exit summary.

  10. More syncing is not fresher syncing. Restic-over-rclone with default parallelism, or two rclone jobs hitting Drive at once, trips Google Drive’s rate limit and everything stalls in quota-retry loops. Throttle the rclone backend, serialize anything that touches Drive, keep the cloud cadence low, and use your own OAuth client ID. The frequent, always-current copy is the local NAS. Drive is the off-site insurance copy.

  11. Scratch space is part of the design. restic’s temp packs default to /tmp. If that is a small root volume, a big pass fails with no space left on device while the remote has plenty of room. Set TMPDIR and RCLONE_TEMP_DIR to the big volume.

  12. A job that auto-disables can hide a failure. OpenClaw cron disables a job after repeated errors. A NAS backup once went dark this way because unreadable lab files made restic exit 3. Keep an independent staleness alarm that does not depend on the backup job’s own scheduler.

  13. A stale-lock init loop looks like an outage. If a killed restic run leaves a lock and your script falls through to restic init on any access failure, every later run fails the same way. Clear only dead-owner locks with plain restic unlock at the start of each phase.