Files
openclaw/docs/install/backups.md
T
Peter Steinberger 37b4fc8621 feat(backup): recorded runs, scheduled backups, and git-backed versioned snapshots (#122485)
* refactor(infra): extract shared git exec and verified snapshot-copy helpers

Moves the worktrees git wrapper to src/infra/git-exec.ts (with optional
maxOutputBytes for large buffered reads) and the online-backup/sanitize/
VACUUM/verify snapshot step into src/snapshot/openclaw-snapshot-copy.ts so
snapshot backends share one hardened copy path. Behavior-identical moves;
all importers updated.

* feat(snapshot): git-backed versioned SQLite snapshot engine

Deterministic per-table JSONL dumps (PK-ordered, lossless bigint/blob
encoding), verbatim DDL preservation, virtual/shadow-table skipping with
FTS rebuild on restore, secret-table redaction policy, manifest with
per-table row counts and content hashes, and restore verification by
re-serialization. Unchanged data produces no commit.

* feat(backup): recorded runs, freshness surfacing, and scheduled git backups

Every backup attempt is recorded in the previously writer-less backup_runs
table (bounded to 200 rows). openclaw status gains a Backups overview row
and JSON payload; doctor prints an informational hint when no successful
backup is recorded or the newest is stale. New commands: backup git
init/create/log/verify/restore and backup enable/disable, which provision
one idempotent gateway cron job running scheduled git backups.

* fix(state): stop bumping schema_meta.updated_at on unchanged opens

updated_at now records when schema metadata actually changed instead of
when the database was last opened; unconditional bumps dirtied the row on
every open and defeated no-change backup detection.

* docs: document versioned git backups, scheduling, and backup freshness

* fix(backup): satisfy CI ownership checks

* fix(backup): complete CI contract coverage

* fix(backup): complete credential table redaction

* fix(backup): isolate git repository ownership

* fix(backup): persist push degradation

* fix(backup): atomically converge schedules

* fix(status): isolate backup freshness environment

* fix(status): carry scan environment to freshness reads

* fix(backup): harden Git repository ownership

* docs(backup): document Git repository safety

* fix(backup): non-creating outcome log and origin preflight for pushed schedules

Recording a backup outcome never bootstraps an absent state database (a
failed backup on a fresh host would otherwise create a blank DB that a
retry treats as real input), and backup enable --push now requires the
repository to have an origin remote, pointing at backup git init --remote
instead of scheduling permanently degraded pushes.

* refactor(worktrees): use shared git exec helpers

* refactor(worktrees): remove unused git buffer wrapper

* refactor(worktrees): consume buffered git helper

* feat(backup): redact pushed schedules by default

Unattended recurring pushes retain credential-bearing tables durably in
remote Git history, so backup enable --push now defaults to
--exclude-secrets; --include-secrets is the explicit full-fidelity
override (still warned). Local non-push schedules keep full fidelity for
complete restores.

* fix(backup): redact audit HMAC and OAuth pending state; tolerate absent backup_runs

Adds audit_identity_keys (audit HMAC key) and mcp_oauth_pending_authorizations
(live OAuth callback state) to the redaction inventory, and makes read-only
backup freshness treat a same-version database without the additive
backup_runs table as no recorded backups instead of failing before a
writable open converges the schema.

* fix(backup): restrict schedules to local gateways

* fix(snapshot): harden Git restore and redaction

* fix(backup): block pushes of adopted history

* fix(backup): contain commits and pairing secrets
2026-08-12 08:11:22 -07:00

14 KiB

summary, read_when, title
summary read_when title
Back up OpenClaw state: archives, per-database snapshots, scheduling, offsite copies, and continuous replication
You want a backup routine for an OpenClaw install instead of a one-off archive
You want scheduled, offsite, or continuous backups without copying the whole database every time
You need to restore OpenClaw state from a backup
Backups

Backups

OpenClaw keeps its authoritative state in SQLite: one global control-plane database plus one database per agent, all under the state directory (usually ~/.openclaw). See Database schemas for the exact layout. This guide covers protecting that state: one-off archives, per-database snapshots, scheduling, offsite copies, and continuous replication for installs that should not re-upload whole databases on every backup.

Never copy live .sqlite, -wal, -shm, or -journal files as a backup. The databases are written while the Gateway runs, and raw file copies of a live database can be torn or corrupt. Every supported path below captures committed state safely.

Backups contain auth profiles, channel and provider credentials, session history, and other sensitive records. Store them encrypted, restrict the destination like you restrict the live state directory, and rotate credentials if you suspect a backup leaked. See [Migrating between machines](/install/migrating) for the same rules applied to machine moves.

Choose a path

  • One-off, everything, portable: openclaw backup create archive.
  • One database, compact and verified: openclaw backup sqlite create.
  • Versioned and incremental by content: openclaw backup git create.
  • Regular protection: provision the Gateway-owned backup automation.
  • Continuous, incremental, seconds of data loss: replicate the databases with Litestream.

Full archives

openclaw backup create --output ~/Backups/openclaw --verify

This writes a timestamped .tar.gz covering state, config, credentials, sessions, and (by default) workspaces, then validates the archive manifest and payload. SQLite databases inside the archive are captured with SQLite's online backup API and compacted, so the archive is safe to create while the Gateway runs. Backup CLI documents every flag, the volatile files that are intentionally skipped, and verification details.

Archives are full copies: each run re-uploads everything. They are the right tool before an update, reset, uninstall, or machine move, and a reasonable daily routine for small installs. For large workspaces or frequent backups, prefer snapshots or continuous replication below.

Per-database snapshots

openclaw backup sqlite create --global --repository ~/Backups/openclaw-sqlite
openclaw backup sqlite create --agent main --repository ~/Backups/openclaw-sqlite

Each run publishes one verified snapshot directory (manifest.json plus database.sqlite) into the repository directory. Snapshots are vacuumed, so deleted-page remnants do not inflate them, and every snapshot records a SHA-256 that openclaw backup sqlite verify rechecks later.

Snapshot repositories are local directories. Scheduling, upload, retention, and restore-on-boot are intentionally left to the operator; the sections below cover them.

Schedule backups

The recommended schedule is one Gateway-owned automation. This example backs up every registered database daily and pushes the current branch to origin. Pushing requires the repository to have an origin remote first, so initialize it once before enabling a pushed schedule:

openclaw backup git init --repository ~/Backups/openclaw-git --remote git@github.com:you/openclaw-backups.git
openclaw backup enable --repository ~/Backups/openclaw-git --every 24h --push

backup enable --push refuses to schedule when no origin remote is configured, so a fresh install cannot silently create a schedule whose pushes always fail.

Pushed schedules redact credential-bearing tables by default: an unattended recurring push would otherwise retain credentials durably in remote Git history. Pass --include-secrets to schedule full-fidelity remote backups when you accept that tradeoff and the remote is private; restores from redacted history require re-pairing devices and re-authenticating providers afterward. Local (non-push) schedules keep full fidelity so restores are complete.

Use --global-only or --agent <id> to narrow the scope. Add --exclude-secrets for a redacted Git history. Re-running the command updates the fixed scheduled job instead of creating another one. Disable it with:

openclaw backup disable

The Gateway must be reachable while enabling or disabling the schedule. There is no local fallback scheduler.

As an alternative, use your platform scheduler directly. A nightly cron example that snapshots the control-plane database and the main agent database:

0 3 * * * openclaw backup sqlite create --global --repository "$HOME/Backups/openclaw-sqlite" --json >> "$HOME/Backups/openclaw-backup.log" 2>&1
5 3 * * * openclaw backup sqlite create --agent main --repository "$HOME/Backups/openclaw-sqlite" --json >> "$HOME/Backups/openclaw-backup.log" 2>&1

On macOS, a launchd job works the same way; on servers provisioned from the hosting guides, a systemd timer is the natural fit. --json emits one machine-readable result per run, so the log doubles as a backup audit trail. Prune old snapshot directories on your own retention schedule.

Every non-dry-run archive, local SQLite snapshot, and Git backup attempt is also recorded in the shared state database. openclaw status shows the newest attempt, and openclaw doctor suggests a one-off or scheduled backup when no successful run is recorded or the newest success is more than 14 days old.

Copy backups offsite

Archives and snapshot repositories are plain files, so any sync tool works. An rclone example targeting an S3-compatible bucket:

rclone sync ~/Backups/openclaw-sqlite remote:openclaw-backups/sqlite

Because every archive and local snapshot is a full copy, offsite syncs re-upload each new backup in full. Deduplicating backup tools such as restic reduce storage at the destination but still read full snapshots as input. When upload size per backup matters, use Git-backed snapshots or continuous replication.

Versioned backups to a Git repository

Git-backed backups dump each selected database into deterministic schema.sql, manifest.json, and per-table JSONL files, then create one commit for the whole run. Unchanged database content produces no commit, so Git stores and pushes only content changes by construction. OpenClaw stages only the backup-owned global and agents paths, not unrelated files elsewhere in the repository.

openclaw backup git init --repository ~/Backups/openclaw-git --remote <private-git-url>
openclaw backup git create --repository ~/Backups/openclaw-git --all --push
openclaw backup git log --repository ~/Backups/openclaw-git

Use a repository dedicated to OpenClaw backups. Existing global/ and agents/<agentId>/ scopes must be empty or contain a valid schema-version-1 OpenClaw backup manifest. OpenClaw refuses to replace any other scope, and an --all run validates every existing agent scope before deleting stale backup-owned entries.

The repository root must be owned by the current user and must not be group- or world-writable. This is checked during init and every create. On POSIX systems, confirm ownership and run chmod 700 <repository> to repair unsafe permissions.

The repository is ordinary Git and can use any remote, including GitHub. Keep the remote private: the default dump includes auth profiles, tokens, and other credential-bearing state. --exclude-secrets omits the documented secret tables when a redacted history is more useful than a credential-complete backup; see Backup CLI for the exact list.

Verify or restore one database at any commit without overwriting a live file:

openclaw backup git verify --repository ~/Backups/openclaw-git --ref <commit> --global
openclaw backup git restore --repository ~/Backups/openclaw-git --ref <commit> --agent main --target ./restored-agent.sqlite

Git restore converges derived search state: it rebuilds content-backed FTS5 indexes, leaves transcript projection state for Gateway startup reconciliation, and leaves vector tables for memory indexing to recreate. It then verifies table hashes, SQLite integrity, and foreign keys.

Continuous replication with Litestream

Litestream is an open-source replication daemon for SQLite. It runs alongside the Gateway with no OpenClaw changes: it watches each database's write-ahead log and streams incremental changes to object storage, with periodic snapshots so restores stay fast. Only changed pages leave the machine, which makes it the right tool when backups must not re-upload whole databases.

OpenClaw's databases run in WAL mode, which is Litestream's one hard requirement. A minimal litestream.yml replicating the control-plane database and one agent database to an S3-compatible bucket:

dbs:
  - path: /home/user/.openclaw/state/openclaw.sqlite
    replicas:
      - url: s3://openclaw-backups/state
  - path: /home/user/.openclaw/agents/main/agent/openclaw-agent.sqlite
    replicas:
      - url: s3://openclaw-backups/agents/main

Run litestream replicate under your process supervisor, one entry per database you care about. To recover, restore to a fresh path and activate it offline:

litestream restore -o ./restored-openclaw.sqlite s3://openclaw-backups/state

Litestream replicates database bytes only. Config, credentials files, and workspaces still need one of the file-based paths above, and the replicated data is as sensitive as the archives, so apply the same bucket access and encryption rules.

Restore

Restore is deliberately explicit; nothing overwrites live state in place.

Restore a full archive

Start only from an archive you created or otherwise trust. openclaw backup verify checks archive structure and payload layout, but it does not authenticate the archive or make untrusted content safe.

Before a full restore, review What gets backed up. Archives intentionally omit volatile files, plugin dependency trees, and installer-managed runtime roots such as state-local tmp/. Recreate those artifacts after restore.

Verify before extracting, then stage the archive in a private temporary directory:

set -euo pipefail

ARCHIVE=./2026-03-09T08-00-00.000+08-00-openclaw-backup.tar.gz

openclaw backup verify "$ARCHIVE"

restore_dir="$(mktemp -d -t openclaw-restore.XXXXXX)"
trap 'rm -rf "$restore_dir"' EXIT

tar -xzf "$ARCHIVE" -C "$restore_dir"
manifest_path="$(find "$restore_dir" -mindepth 2 -maxdepth 2 -name manifest.json -print -quit)"
test -n "$manifest_path"
cat "$manifest_path"

Treat the staging directory as sensitive. It can contain credentials, auth profiles, sessions, and workspace data. The trap removes it when the shell exits.

The manifest records archiveRoot, the original paths under paths, and an assets[] list. Each asset includes its kind, original sourcePath, and archivePath inside the tarball. Use those fields as the source of truth; do not derive the archive root from the archive filename.

The archive layout is:

<archive-root>/manifest.json
<archive-root>/payload/posix/<absolute-source-path-without-leading-slash>/...
<archive-root>/payload/windows/<DRIVE>/<rest>/...
<archive-root>/payload/relative/<relative-source-path>/...

Before copying files back, stop the Gateway and any node hosts that use them. Make a fresh backup of the current state or move the current directories aside. Restore the smallest set of assets needed.

For example, this restores the state asset to the current user's default state directory. The target stays absent until cp -a creates it, preserving the staged directory's mode and metadata:

set -euo pipefail

state_archive_path="$(
  node -e 'const fs = require("node:fs"); const manifest = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(manifest.assets.find((asset) => asset.kind === "state")?.archivePath ?? "");' "$manifest_path"
)"
test -n "$state_archive_path"

state_source="$restore_dir/$state_archive_path"
state_target="$HOME/.openclaw"
state_backup="$HOME/.openclaw.pre-restore.$(date +%s)"

test -d "$state_source"
openclaw gateway stop

if [ -e "$state_target" ] || [ -L "$state_target" ]; then
  mv "$state_target" "$state_backup"
fi
test ! -e "$state_target"
test ! -L "$state_target"
cp -a "$state_source" "$state_target"

openclaw doctor
openclaw gateway start
openclaw health
openclaw status

For a same-machine restore, the manifest sourcePath values are usually the intended targets. On a new machine or under a different home directory, choose the new targets first, then copy only the matching asset payloads. Typical full-restore targets are the state directory, active config file, credentials directory, and workspace directories. See Updating for the rollback workflow.

Restore a database

For a snapshot, openclaw backup sqlite restore <snapshot-directory> --target <new-database-path> writes a re-verified database to a fresh target. For Git history, openclaw backup git restore --repository <dir> --ref <commit> (--global | --agent <id>) --target <new-database-path> materializes and verifies a fresh database. For Litestream, litestream restore writes a fresh database file. Move the result into place while the Gateway is stopped, then start the Gateway and check openclaw health and openclaw doctor.

After restoring onto a different OpenClaw version, preflight the database first with openclaw database preflight; see Database schemas.