OpenClaw Integration with Backblaze B2
  • 16 Mar 2026
  • Dark
    Light

OpenClaw Integration with Backblaze B2

  • Dark
    Light

Article summary

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

index.ts

Plugin entry point; hooks, tool registration, debounce wiring

openclaw.plugin.json

Plugin manifest; config schema, UI hints

src/types.ts

Config and manifest types; SAFETY_PREFIX constant

src/b2-client.ts

S3-compatible B2 client (AWS Sig V4 signing via node:crypto)

src/gatherer.ts

Walks the state directory and collects syncable files by pattern

src/sqlite-snapshot.ts

Safe .backup() wrapper for SQLite databases

src/manifest.ts

SHA-256 hashing and manifest compute, diff, and serialization

src/encryption.ts

AES-256-GCM encryption and decryption with scrypt key derivation

src/debounce.ts

Push rate limiter (5-minute gate for compaction triggers)

src/snapshots.ts

Lists, prunes, and filters snapshots in B2

src/push.ts

Uploads changed files to B2 (parallel, 8 concurrent)

src/pull.ts

Downloads and restores snapshots from B2 (with safety snapshot)

src/service.ts

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 stateDir

Installation 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:

  1. Create a Backblaze bucket that will store OpenClaw snapshots.

  2. 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.

  3. Save the following values for use in the plugin configuration:

    • keyId

    • applicationKey

    • bucket name

Install

openclaw plugins install openclaw-b2-backup

Configuration 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

keyId

string

—

Yes

Backblaze B2 application key ID

applicationKey

string

—

Yes

Backblaze B2 application key
Also used as the source for encryption key derivation via scrypt.

bucket

string

—

Yes

Backblaze B2 bucket name

region

string

Auto-detected

No

Backblaze B2 region
If omitted, derived from the b2_authorize_account v3 API response.
Backblaze recommends that you leave this as unset.

prefix

string

"openclaw-backup"

No

Object key prefix in the bucket
Allows multiple OpenClaw instances to share a bucket.

schedule

string

"daily"

No

"daily" (midnight), "weekly" (Sunday midnight), or any cron expression

encrypt

boolean

true

No

AES-256-GCM encryption before upload
Manifests are always unencrypted.

keepSnapshots

integer

10

No

Number of snapshots retained
Oldest are auto-pruned after each push. Safety snapshots are never auto-pruned.

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.

  1. Gather eligible files by walking the state directory with gatherFiles(stateDir) and returning files that match the configured include patterns (see File Gathering).

  2. Create consistent SQLite snapshots by copying any .sqlite files to a temporary directory using the .backup() API via snapshotSqlite().
    This ensures databases are captured in a consistent state rather than mid-WAL write.

  3. 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.

  4. Load the previous manifest from {stateDir}/.b2-backup-manifest.json to compare against the current state. (Skip this step for safety snapshots, which always perform a full upload.)

  5. Diff manifests using diffManifests(prev, current) to determine added, changed, and deleted files.

  6. 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}.

  7. Upload the manifest unencrypted to {prefix}/{timestamp}/manifest.json.

  8. Persist the new manifest locally using writeJsonFileAtomically(). (Skip for safety snapshots.)

  9. Prune old snapshots by calling pruneSnapshots() to remove snapshots exceeding keepSnapshots. (Skip for safety snapshots.)

  10. 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.

  1. Create a safety snapshot (if applicable).

    Before modifying local state, the current state is pushed to {prefix}/safety-{timestamp}/ with skipPrune: 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.)

  2. Download the snapshot manifest from {prefix}/{timestamp}/manifest.json.

  3. 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.

  4. Download all files that are missing or whose hashes differ from the manifest.
    If a file begins with the B2EN magic header, it is decrypted. Otherwise, it is treated as unencrypted for backward compatibility.

  5. Compute the SHA-256 hash of the decrypted plaintext and verify it matches the manifest entry.
    Mismatched files are skipped with a warning.

  6. Create any required parent directories and write the restored files to disk in place.

  7. Save the restored manifest to .b2-backup-manifest.json so 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 ciphertext

Component

Size

Purpose

Magic

4 bytes

B2EN identifies encrypted data

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

encrypt(plaintext, applicationKey)

Returns Buffer with magic header + encrypted data

decrypt(data, applicationKey)

Returns decrypted Buffer
Throws if magic header missing.

isEncrypted(data)

Returns true if data starts with B2EN magic and is at least 64 bytes Used by pull to auto-detect encrypted vs. unencrypted files.

deriveKey(applicationKey, salt)

Returns 32-byte key from scrypt
Exported for testing.

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

/^openclaw\.json$/

Main config file

/^openclaw\.json\.bak/

Config backup rotations

/^agents\/[^/]+\/sessions\/[^/]+\.jsonl$/

Session transcript files

/^agents\/[^/]+\/sessions\/sessions\.json$/

Session routing metadata

/^agents\/[^/]+\/memory\/[^/]+\.sqlite$/

Memory databases (vector search)

/^agents\/[^/]+\/agent\//

Agent runtime config

/^workspace\//

Primary workspace (SOUL.md, AGENTS.md, MEMORY.md)

/^workspace-[^/]+\//

Named workspaces

/^cron\//

Scheduled tasks

/^hooks\//

Hook scripts

Exclude Patterns

Pattern

Reason

/^credentials\//

Secrets stay per-machine

/agents\/[^/]+\/agent\/auth-profiles\.json$/

Auth tokens stay per-machine

/^media\//

Ephemeral (2-min TTL), not worth syncing

/^extensions\//

Install plugins independently per machine

/\.lock$/, /\.tmp$/

Transient lock/temp files

/-wal$/, /-shm$/

SQLite WAL/SHM files (handled by .backup())

/(^|\/)\.DS_Store$/

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

"daily" (default)

0 0 * * *

Midnight daily

"weekly"

0 0 * * 0

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()):

  1. Authorize with Backblaze B2 and verify bucket access via headBucket().

  2. Gather files in the state directory.

  3. If zero files are found, check Backblaze B2 for existing snapshots.

  4. If snapshots exist, call pullLatest() with skipSafety: 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 run

Test Coverage

Test File

Covers

encryption.test.ts

Round-trip encrypt/decrypt, isEncrypted detection, backward-compatible passthrough of unencrypted data

manifest.test.ts

Manifest computation, diffing (added/changed/deleted), serialization round-trips

snapshots.test.ts

Snapshot listing, pruning logic, getLatestSnapshot

gatherer.test.ts

Include/exclude pattern matching, directory walking

b2-client.test.ts

AWS Sig V4 signing correctness, XML list response parsing, region discovery

debounce.test.ts

Debounce gate timing, acquire/release, remainingMs

index.test.ts

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.


Was this article helpful?