OpenClaw Integration with Backblaze B2
    • 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?