- Print
- DarkLight
OpenClaw Integration with Backblaze B2
- Print
- DarkLight
openclaw-b2-backup is an OpenClaw gateway plugin that automatically snapshots the entire state directory to Backblaze B2 Cloud Storage. It runs inside the gateway process, requires no external tools or scripts, and has a single runtime dependency (croner for scheduling). The Backblaze B2 client is a hand-rolled AWS Signature V4 implementation using only node:crypto.
The plugin handles three operations:
push(incremental upload of changed files)pull(download and restore a snapshot)auto-restore(detect an empty state directory on startup and automatically restore the latest snapshot).
All file contents are AES-256-GCM encrypted before upload by default.
This GitHub repository is a plugin for the OpenClaw gateway that automatically snapshots and backs up the entire OpenClaw state directory to Backblaze B2. It runs inside the gateway process, supports incremental uploads and scheduled backups, and encrypts all file contents with AES-256-GCM before upload by default. The plugin also supports snapshot listing, rollback, and automatic restore on a new or empty machine.
Architecture and Module Map
File | Description |
|---|---|
| Plugin entry point; hooks, tool registration, debounce wiring |
| Plugin manifest; config schema, UI hints |
| Config and manifest types; SAFETY_PREFIX constant |
| S3-compatible B2 client (AWS Sig V4 signing via node:crypto) |
| Walks the state directory and collects syncable files by pattern |
| Safe |
| SHA-256 hashing and manifest compute, diff, and serialization |
| AES-256-GCM encryption and decryption with scrypt key derivation |
| Push rate limiter (5-minute gate for compaction triggers) |
| Lists, prunes, and filters snapshots in B2 |
| Uploads changed files to B2 (parallel, 8 concurrent) |
| Downloads and restores snapshots from B2 (with safety snapshot) |
| Background service with cron scheduler and auto-restore on start |
Data Flow
Push:
gatherFiles(stateDir)
→ snapshotSqlite(.sqlite files)
→ computeManifest(SHA-256 hashes on plaintext)
→ diffManifests(prev, current)
→ encrypt(changed files, applicationKey)
→ b2.putObject(encrypted data)
→ b2.putObject(manifest.json, unencrypted)
→ pruneSnapshots(keep N newest)Pull:
b2.getObject(manifest.json)
→ for each file: compare local hash
→ b2.getObject(file)
→ decrypt(if B2EN header present)
→ verify SHA-256 against manifest
→ write to stateDirInstallation and Configuration
Install the plugin and configure it with your Backblaze B2 credentials to enable encrypted backups of the OpenClaw state directory.
Prerequisites
Before installing and configuring the OpenClaw Backblaze B2 backup plugin, complete the following steps in the Backblaze web console:
Create a Backblaze bucket that will store OpenClaw snapshots.
Create an application key with access to the bucket.
The key should allow the plugin to list, read, write, and delete files in the bucket.
Save the following values for use in the plugin configuration:
keyId
applicationKey
bucket name
Install
openclaw plugins install openclaw-b2-backupConfiguration Schema
The plugin is configured in ~/.openclaw/openclaw.json under the openclaw-b2-backup key. The installer creates the entry; the user adds the config block:
{
"openclaw-b2-backup": {
"enabled": true,
"config": {
"keyId": "004a...",
"applicationKey": "K004...",
"bucket": "my-openclaw-backups"
}
}
}Field | Type | Default | Req? | Description |
|---|---|---|---|---|
| string | — | Yes | Backblaze B2 application key ID |
| string | — | Yes | Backblaze B2 application key |
| string | — | Yes | Backblaze B2 bucket name |
| string | Auto-detected | No | Backblaze B2 region |
| string |
| No | Object key prefix in the bucket |
| string |
| No |
|
| boolean |
| No | AES-256-GCM encryption before upload |
| integer |
| No | Number of snapshots retained |
Config Validation
The plugin does not mark required fields in its JSON Schema, because doing so would prevent openclaw plugins install from completing before the user has added credentials. Instead, the register() function validates that keyId, applicationKey, and bucket are present at runtime and logs a warning if any are missing.
Push Flow (Local → Backblaze B2)
The push operation handles incremental backup of the OpenClaw state directory to Backblaze B2 . It gathers eligible files, computes changes since the last snapshot, encrypts updated content, and uploads only what has changed.
Implemented in src/push.ts. It accepts a PushOptions parameter for safety snapshot overrides.
Perform Incremental Push to Backblaze B2
Execute an incremental backup of the OpenClaw state directory to Backblaze B2.
Gather eligible files by walking the state directory with
gatherFiles(stateDir)and returning files that match the configured include patterns (see File Gathering).Create consistent SQLite snapshots by copying any
.sqlitefiles to a temporary directory using the.backup()API viasnapshotSqlite().
This ensures databases are captured in a consistent state rather than mid-WAL write.Compute the current manifest by generating SHA-256 hashes of plaintext file contents (prior to encryption).
Hashing plaintext ensures incremental diffing works correctly, since encryption uses random IVs/salts and produces different ciphertext for identical input.Load the previous manifest from
{stateDir}/.b2-backup-manifest.jsonto compare against the current state. (Skip this step for safety snapshots, which always perform a full upload.)Diff manifests using
diffManifests(prev, current)to determine added, changed, and deleted files.Upload added and changed files in parallel batches of 8
(CONCURRENCY = 8).Each file is encrypted with AES-256-GCM if
config.encrypt !== false.Object keys follow the pattern:
{prefix}/{timestamp}/{relativePath}.
Upload the manifest unencrypted to
{prefix}/{timestamp}/manifest.json.Persist the new manifest locally using
writeJsonFileAtomically(). (Skip for safety snapshots.)Prune old snapshots by calling
pruneSnapshots()to remove snapshots exceedingkeepSnapshots. (Skip for safety snapshots.)Clean up temporary files by removing the SQLite snapshot directory.
PushOptions
type PushOptions = {
prefixOverride?: string; // Override prefix (used for safety snapshots)
skipPrune?: boolean; // Skip pruning (safety snapshots never auto-pruned)
};Concurrency
Files are uploaded in parallel batches of 8. This ensures the push operation completes within the OpenClaw gateway’s ~5-second shutdown timeout. Sequential uploads of 30+ files could exceed this limit, whereas parallel batches of 8 typically complete in 2–3 seconds for a standard state directory.
Pull Flow (Backblaze B2 → Local)
The pull operation restores a snapshot from Backblaze B2 to the local OpenClaw state directory, replacing or updating files as needed to match the selected snapshot.
Implemented in src/pull.ts. It provides two entry points: pullLatest() (restores the most recent snapshot) and pullSnapshot() (restores a specific timestamp).
Restore Workflow (Pull from Backblaze B2)
The restore workflow retrieves a snapshot from Backblaze B2 and updates the local OpenClaw state directory to match the selected snapshot, performing integrity checks and safety safeguards throughout the process.
Create a safety snapshot (if applicable).
Before modifying local state, the current state is pushed to
{prefix}/safety-{timestamp}/withskipPrune: true.This guarantees recovery in case of a failed or incorrect rollback. (Skipped during auto-restore, since an empty state directory has nothing to preserve.)
Download the snapshot manifest from
{prefix}/{timestamp}/manifest.json.Compute the SHA-256 hash of each corresponding local file (if it exists) and compare it to the hash recorded in the manifest.
Files that already match are skipped.Download all files that are missing or whose hashes differ from the manifest.
If a file begins with theB2ENmagic header, it is decrypted. Otherwise, it is treated as unencrypted for backward compatibility.Compute the SHA-256 hash of the decrypted plaintext and verify it matches the manifest entry.
Mismatched files are skipped with a warning.Create any required parent directories and write the restored files to disk in place.
Save the restored manifest to
.b2-backup-manifest.jsonso that the next push operation can perform an accurate incremental diff.
PullOptions
type PullOptions = {
skipSafety?: boolean; // Skip safety snapshot (used during auto-restore)
};Encryption
The plugin encrypts all file contents before upload to Backblaze B2 to ensure confidentiality and integrity of backup data.
Implemented in src/encryption.ts. All encryption uses AES-256-GCM with scrypt key derivation from the applicationKey.
Binary Format
[MAGIC 4B "B2EN"][SALT 32B][IV 12B][AUTH_TAG 16B][CIPHERTEXT ...]
Total header: 64 bytes before ciphertextComponent | Size | Purpose |
|---|---|---|
Magic | 4 bytes |
|
Salt | 32 bytes | Random salt for scrypt key derivation |
IV | 12 bytes | Random initialization vector for AES-256-GCM |
Auth Tag | 16 bytes | GCM authentication tag (integrity + authenticity) |
Ciphertext | variable | Encrypted file contents |
Key Derivation
scrypt(applicationKey, randomSalt, keylen=32, N=16384, r=8, p=1)Each file gets a unique random salt, so each file derives a unique encryption key. This means identical plaintext files produce entirely different ciphertext. The user never manages a separate encryption key; it is derived from the Backblaze B2 application key they already have.
Function | Description |
|---|---|
| Returns |
| Returns decrypted |
| Returns |
| Returns 32-byte key from scrypt |
Unencrypted Manifests
Manifest files contain only file paths and SHA-256 hashes (no file content). They remain unencrypted so incremental diffing works correctly, since each encryption uses a random salt and IV and ciphertext cannot be compared to detect changes.
Backblaze B2 Client and S3 Signing
The plugin includes a minimal S3-compatible client used to communicate directly with Backblaze B2. This client handles object uploads, downloads, listing, and deletion while generating authenticated requests using AWS Signature Version 4.
The implementation is located in src/b2-client.ts and performs request signing using Node’s built-in node:crypto module. It does not rely on the AWS SDK or any external S3 libraries.
Client Interface
type B2Client = {
putObject(bucket: string, key: string, body: Uint8Array, contentType: string): Promise<void>;
getObject(bucket: string, key: string): Promise<Buffer>;
listObjects(bucket: string, prefix: string): Promise<B2ObjectEntry[]>;
deleteObject(bucket: string, key: string): Promise<void>;
headBucket(bucket: string): Promise<void>;
};Region Discovery
If a region is not explicitly configured, the client automatically determines it by calling the b2_authorize_account API (v3) and reading the s3ApiUrl field from the response. For bucket-scoped application keys, this value is nested under apiInfo.storageApi.s3ApiUrl. To maintain backward compatibility with older responses, the client checks both possible locations.
const s3ApiUrl = data.apiInfo?.storageApi?.s3ApiUrl ?? data.s3ApiUrl;
const match = s3ApiUrl?.match(/s3\.([^.]+)\.backblazeb2\.com/);Request Signing
Standard AWS Signature V4 flow: canonical request → string to sign → HMAC chain (date → region → service → "aws4_request") → signature. The x-amz-content-sha256 header is always set to the SHA-256 of the request body.
List Objects Pagination
Uses S3 ListObjectsV2 (list-type=2) with continuation-token pagination. The XML response is parsed with regex to extract Key, Size, and LastModified from <Contents> blocks. Pagination continues while <IsTruncated>true</IsTruncated> is present.
File Gathering and Exclusions
The plugin determines which files in the OpenClaw state directory should be included in backups by applying a set of regex-based include and exclude patterns.
This logic is implemented in src/gatherer.ts.
Include Patterns
Pattern | Matches |
|---|---|
| Main config file |
| Config backup rotations |
| Session transcript files |
| Session routing metadata |
| Memory databases (vector search) |
| Agent runtime config |
| Primary workspace (SOUL.md, AGENTS.md, MEMORY.md) |
| Named workspaces |
| Scheduled tasks |
| Hook scripts |
Exclude Patterns
Pattern | Reason |
|---|---|
| Secrets stay per-machine |
| Auth tokens stay per-machine |
| Ephemeral (2-min TTL), not worth syncing |
| Install plugins independently per machine |
| Transient lock/temp files |
| SQLite WAL/SHM files (handled by .backup()) |
| macOS metadata |
Directory-level early exits skip credentials/, media/, extensions/, and node_modules/ entirely to avoid unnecessary traversal.
Manifest Format and Diffing
The plugin tracks snapshot contents and detects changes between backups using a manifest file format and diffing logic implemented in src/manifest.ts.
Format
{
"version": 1,
"timestamp": "2026-02-26T00:00:00.000Z",
"files": {
"openclaw.json": { "hash": "a3f1...", "size": 4821 },
"workspace/MEMORY.md": { "hash": "b7e2...", "size": 12034 },
...
}
}Hashes are always SHA-256 of the plaintext file content (never the encrypted ciphertext). This is stored unencrypted in Backblaze B2 at {prefix}/{timestamp}/manifest.json.
Diffing
The diffManifests(prev, current) function compares the previous and current manifests to determine which files have changed. It produces three lists: added (present in the current manifest but not the previous one), changed (files whose hashes differ), and deleted (present in the previous manifest but not the current one). Only added and changed files are uploaded. Deleted files are recorded in the new manifest but are not removed from existing snapshots in Backblaze B2; they simply do not appear in the new snapshot.
Hashing
Files are hashed via a streaming createReadStream + createHash("sha256") pipeline, so large files don't need to be buffered in memory for hashing.
Sync Triggers and Scheduling
The plugin can trigger synchronization through scheduled tasks and gateway lifecycle events.
Cron Schedule (service.ts)
The background service resolves the schedule config value to a cron expression.
Value | Cron Expression | Meaning |
|---|---|---|
|
| Midnight daily |
|
| Sunday midnight |
Custom string | Passed through | Any valid cron expression |
Scheduling uses the croner library (the only runtime dependency).
Gateway Stop Hook (index.ts)
Registered via api.on("gateway_stop", ...). Performs a final push before the gateway exits. Must complete within the gateway's ~5-second shutdown timeout, which is why uploads are performed in parallel.
Before Compaction Hook (index.ts)
Registered via api.on("before_compaction", ...). Protected by a 5-minute debounce gate (src/debounce.ts) to prevent rapid-fire compaction events from queuing multiple pushes. If the debounce gate has been acquired within the last 5 minutes, the push is skipped and a debug log is emitted with the remaining cooldown time.
Auto-Restore (service.ts)
On service start, if gatherFiles(stateDir) returns zero files and Backblaze B2 has at least one snapshot, the latest snapshot is pulled automatically with skipSafety: true (nothing to preserve from an empty state dir).
Snapshot Management and Pruning
The plugin manages snapshot listing and retention to ensure that older backups are automatically cleaned up while preserving recent snapshots.
This functionality is implemented in src/snapshots.ts.
Object Key Layout
{bucket}/
{prefix}/
2026-02-25T00-00-00Z/ # Regular snapshot
manifest.json
openclaw.json
workspace/MEMORY.md
...
2026-02-26T00-00-00Z/ # Regular snapshot
manifest.json
...
safety-2026-02-26T12-30-00Z/ # Safety snapshot (never auto-pruned)
manifest.json
...Timestamp Format
ISO 8601 with colons replaced by hyphens and milliseconds stripped: 2026-02-26T00-00-00Z. This ensures the timestamps are valid as S3 key components and sort lexicographically.
Listing
listSnapshots() calls b2.listObjects(bucket, prefix + "/"), extracts unique timestamp directories from the key paths, and returns them sorted. Safety snapshots have the safety- prefix in their timestamp directory name.
Pruning
The pruneSnapshots(b2, bucket, prefix, keep) function lists all snapshots under the prefix, keeps the keep most recent, and deletes all objects within older snapshots. Safety snapshots are stored under a different prefix pattern (safety-{timestamp}) and are never included in the pruning list because they appear as separate top-level timestamp entries that don't match the regular timestamp format.
Note
Safety snapshots are never auto-pruned. Over time, they accumulate. Users should periodically clean up old safety snapshots manually using the Backblaze B2 CLI or Backblaze web console if storage is a concern.
Agent Tool (b2_rollback)
The plugin exposes a tool named b2_rollback that allows the OpenClaw agent to list available snapshots and restore a selected snapshot from Backblaze B2.
It is registered in index.ts using api.registerTool().
list-snapshots
Returns all snapshot timestamps (regular and safety) for the configured prefix. The agent can present these to the user conversationally.
// Input
{ "action": "list-snapshots" }
// Output
{
"result": "Found 5 snapshot(s):\n - 2026-02-22T00-00-00Z\n ...",
"snapshots": ["2026-02-22T00-00-00Z", ...]
}restore
Requires a timestamp parameter. Creates a safety snapshot of the current state, then restores the specified snapshot.
// Input
{ "action": "restore", "timestamp": "2026-02-25T00-00-00Z" }
// Output
{ "result": "Restored snapshot 2026-02-25T00-00-00Z" }Auto-Restore on New Machine
When the plugin service starts (service.ts → start()):
Authorize with Backblaze B2 and verify bucket access via
headBucket().Gather files in the state directory.
If zero files are found, check Backblaze B2 for existing snapshots.
If snapshots exist, call
pullLatest()withskipSafety: true.
This means the workflow for moving to a new machine is: install the plugin, add Backblaze B2 credentials, restart the gateway. The plugin detects the empty state directory, finds the existing snapshots, and restores the latest one automatically.
Security Considerations
The following sections describe how encryption, key management, credential handling, and bucket configuration affect the overall security of the system.
Encryption at Rest
All file data is AES-256-GCM encrypted before leaving the machine (when encrypt is true, which is the default). Each file gets a random 32-byte salt and 12-byte IV, so identical files produce different ciphertext. The GCM authentication tag provides both integrity and authenticity verification.
Key Management
The encryption key is derived from the Backblaze B2 applicationKey via scrypt. There is no separate encryption key to manage, store, or lose. If the user rotates their B2 application key, old snapshots encrypted with the previous key become undecryptable. Users should retain old keys if they need to access historical snapshots.
Warning
Rotating your Backblaze B2 application key will make all existing encrypted snapshots undecryptable. If you rotate keys, either keep the old key documented for historical access or restore from the latest snapshot before rotating and then create a new backup using the new key.
Credentials Exclusion
The credentials/ directory and auth-profiles.json are excluded from sync by design. Secrets stay per-machine. When restoring to a new machine, the user must re-authenticate with their providers.
Manifest Exposure
Manifest files are unencrypted and contain file paths and SHA-256 hashes (no file content). An attacker with read access to the bucket could see the directory structure and detect file changes, but could not read file content.
Recommended Backblaze B2 Configuration
Use an application key scoped to a single bucket to follow the principle of least-privilege access. The plugin works with bucket-scoped keys, and the region is automatically detected from the authorize response. Do not use the master application key.
Testing
The project includes a suite of automated tests covering the main backup and restore functionality.
The project includes 66 automated tests across 7 test files. Run the test suite with the following command:
pnpm test # or: npx vitest runTest Coverage
Test File | Covers |
|---|---|
| Round-trip encrypt/decrypt, |
| Manifest computation, diffing (added/changed/deleted), serialization round-trips |
| Snapshot listing, pruning logic, |
| Include/exclude pattern matching, directory walking |
| AWS Sig V4 signing correctness, XML list response parsing, region discovery |
| Debounce gate timing, acquire/release, |
| Plugin registration, hook wiring, tool registration |
Troubleshooting
This section lists common errors and troubleshooting steps for the plugin.
b2: could not determine region from authorize response
The Backblaze B2 authorize API did not return an s3ApiUrl in the expected location. This typically happens with very old API versions. The client checks both apiInfo.storageApi.s3ApiUrl (v3) and the top-level s3ApiUrl (v2). If neither is present, set the region config field explicitly (for example, "us-west-004").
b2 authorize failed (401)
Invalid Backblaze B2 credentials. Verify keyId and applicationKey are correct. If using a bucket-scoped key, ensure the key has listBuckets, listFiles, readFiles, writeFiles, and deleteFiles capabilities on the target bucket.
b2 headBucket failed
The plugin could not access the configured bucket. Check that the bucket exists, the application key has access to it, and the bucket name in config matches exactly.
Push times out on gateway shutdown
The gateway enforces an approximately 5-second shutdown timeout. With parallel uploads (8 concurrent), a typical state directory (30–50 files) completes in 2–3 seconds. If you have an unusually large state directory or a slow network connection, the shutdown push may be interrupted. The daily cron push will capture the state at the next scheduled run.
b2-backup: missing required config
The plugin started but keyId, applicationKey, or bucket is missing from the config block in openclaw.json. Add the config block under the openclaw-b2-backup entry and restart the gateway.
b2-backup: no changes since last push
The local manifest matches the current state. No files have changed since the last successful push. This is normal and expected for most scheduled runs.
Stale config after failed install
If a previous install attempt failed partway through, stale entries may remain in openclaw.json under plugins.entries and plugins.installs. Remove these manually, then re-run openclaw plugins install openclaw-b2-backup.