--- name: b2 description: >- Manage Arizona Computer Guru's (ACG) Backblaze B2 storage account via the B2 Native API v3. Talks to the LIVE production B2 account (accountId 46f69bc61163, region us-west-001) that holds the per-client MSP360/CloudBerry backup destinations. List buckets and application keys, list files / file versions, compute per-bucket stored size, and produce the headline storage-cost report (the mspbackups storage-cost calc). Provision buckets and scoped backup keys and delete buckets/keys (all destructive ops are gated behind --confirm). Read-only by default. Invoke for: "backblaze", "b2", "b2 storage", "bucket", "storage cost", "backup storage", "mspbackups storage", "list buckets b2". --- # Backblaze B2 Skill Standalone CLI client for the Backblaze B2 Native API v3. Talks to the live ACG B2 account. Read-only by default; destructive operations are gated behind `--confirm`. ## Running the CLI This machine's Python launcher is `py` (per identity.json). The scripts also work with `python`/`python3`. ```bash # from the scripts dir, or pass full paths py "$CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" status py "$CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" buckets py "$CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" usage --json ``` Transport auto-selects: uses `httpx` if installed, otherwise stdlib `urllib` (no third-party dependency required). ## Credentials Credentials are NEVER hardcoded. At runtime the client loads them from the SOPS vault: ``` bash "$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh" \ get-field projects/claudetools/backblaze-b2.sops.yaml key_id bash "$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh" \ get-field projects/claudetools/backblaze-b2.sops.yaml credentials.application_key ``` `CLAUDETOOLS_ROOT` resolves from the env var, else `claudetools_root` in `.claude/identity.json`, else the repo root derived from the script's own location (no hardcoded drive letters). For testing you can override with the `B2_KEY_ID` and `B2_APPLICATION_KEY` env vars (both must be set to use the override). Authorization is HTTP Basic against `b2_authorize_account` (username = key id, password = application key). The skill uses the "ClaudeTools" application key (`00146f69bc611630000000009`). ## Cache model (important — the token is a SECRET) After a successful authorize, the CLI caches the auth result at `.claude/skills/b2/.cache/auth.json`: - **Cached:** `authorizationToken` (the bearer token, valid ~24h), the per-account `apiUrl` / `s3ApiUrl` / `downloadUrl`, `accountId`, key capabilities, and key scope (`bucketId` / `namePrefix`). - **TTL:** treated as valid for ~23h. The client re-authorizes when the cache is stale, or automatically on a `401` with `expired_auth_token` / `bad_auth_token` (exactly one re-authorize + retry per call). - **This cache holds a live secret.** It is gitignored twice (root `.gitignore` plus a local `.gitignore` in the skill dir) and must never be committed. The per-account `apiUrl` always comes from the live authorize response — there is no config file for endpoints (project rule). ## Safety gating Destructive subcommands refuse to run without `--confirm`; without it they print what they would do and exit non-zero (3): - `create-bucket [--type allPrivate|allPublic] --confirm` - `create-key --name --capabilities [--bucket ] [--prefix

] [--duration-seconds N] --confirm` - `delete-bucket --confirm` (B2 refuses to delete a non-empty bucket — the error is surfaced verbatim) - `delete-key --confirm` - `delete-prefix [ ...] --confirm` (schedules an IRREVERSIBLE lifecycle purge — see "Prefix purge / lifecycle" below) - `lifecycle-remove [ ...] --confirm` (removes lifecycle rules) `create-key` prints the returned `applicationKey` exactly ONCE with a warning — B2 never shows it again, so store it in the vault immediately. `raw` refuses any method name containing `create` / `delete` / `update` / `hide` / `cancel` unless `--confirm` is passed. `raw` prints the upstream response verbatim, which may carry sensitive data (keys, tokens) — review before pasting into tickets/logs. Destructive calls are NEVER retried automatically. ## Common commands ```bash B2="py $CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" # Status / inventory $B2 status $B2 buckets $B2 keys # Files $B2 files ACG-Internal # latest names $B2 files ACG-Internal --prefix MBS- --limit 50 $B2 files ACG-Internal --versions # all versions # Size + cost $B2 bucket-size ACG-Internal $B2 usage # headline cost report, all buckets $B2 usage --bucket ACG-Dataforth # one bucket $B2 cost --json # alias for usage # Provisioning (gated) $B2 create-bucket ACG-NewClient --confirm $B2 create-key --name acg-newclient-backup \ --capabilities listBuckets,listFiles,readFiles,writeFiles,deleteFiles \ --bucket ACG-NewClient --confirm $B2 delete-bucket ACG-OldClient --confirm $B2 delete-key 00146f69bc611630000000abc --confirm # Lifecycle / prefix purge (see next section) $B2 lifecycle ACG-Internal # read-only: list current rules $B2 delete-prefix ACG-Internal "MBS-/CBB_/" --confirm $B2 lifecycle-remove ACG-Internal "MBS-/CBB_/" --confirm # Power use — any v3 method directly $B2 raw --method b2_list_buckets --body '{"accountId":"46f69bc61163"}' ``` ## Prefix purge / lifecycle Some backup destinations hold **1.2M+ file versions** under a single machine prefix. Per-file deletion (`b2_delete_file_version` per version) is impractical at that scale, so this skill purges a prefix via a **B2 bucket lifecycle rule** instead. B2's own daily lifecycle pass does the deletion server-side. ### How the purge mechanism works A purge rule is: ```json { "fileNamePrefix": "MBS-/CBB_/", "daysFromUploadingToHiding": 1, "daysFromHidingToDeleting": 1 } ``` With both day-counts at `1`, B2's daily lifecycle pass **hides** any file more than 1 day old, then **deletes** hidden files more than 1 day old. Every version under the prefix (all are years old) becomes eligible immediately, so the prefix is fully purged within **~24-48h** (two daily passes). This is **irreversible**: there is no recycle bin and no undo once the pass deletes a version. `fileNamePrefix: ""` means the WHOLE BUCKET — the skill refuses that and any other too-broad prefix (see safety below). ### Lifecycle rules REPLACE, they don't append `b2_update_bucket`'s `lifecycleRules` **replaces the entire array** — it is not additive. The skill therefore always reads the current rules (`b2_list_buckets` scoped to the bucketId), merges the change, and writes the full set back. This read-merge-write of the complete array is what preserves pre-existing rules. **This account's `b2_update_bucket` does NOT support `ifRevisionMatch`** (it returns HTTP 400 `bad_request: unknown field ... ifRevisionMatch`), so the skill sends no optimistic-lock token and writes are **last-write-wins**. There is no 409-conflict retry (no revision guard to violate); the skill makes at most one defensive retry on a transient 5xx, then fails — it never loops. The bucket `revision` is still read and displayed for reference, but is never sent back. B2 allows up to **100** lifecycle rules per bucket; the skill refuses an update that would exceed that. ### Commands ```bash B2="py $CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" # 1. READ-ONLY: see the bucket's current lifecycle rules + revision $B2 lifecycle ACG-Internal $B2 lifecycle ACG-Internal --json # 2. DRY-RUN: what WOULD be scheduled (no --confirm -> exits 3, writes nothing) $B2 delete-prefix ACG-Internal "MBS-/CBB_/" # 3. COMMIT the purge (gated). Idempotent: an identical rule already present # is skipped. You can pass several prefixes at once. $B2 delete-prefix ACG-Internal \ "MBS-/CBB_OLDPC1/" \ "MBS-/CBB_OLDPC2/" --confirm # 4. After ~24-48h, verify the data is gone (size should have dropped) $B2 bucket-size ACG-Internal # 5. Clean up the now-spent purge rule(s) so they don't sit on the bucket $B2 lifecycle-remove ACG-Internal \ "MBS-/CBB_OLDPC1/" \ "MBS-/CBB_OLDPC2/" --confirm ``` ### Safety validations (hard-fail even with --confirm) `delete-prefix` refuses (exit 2) any prefix that is: - empty, `"/"`, `"*"`, or **contains no `/`** — too broad (would risk a whole-bucket or whole-account-tree purge); - exactly an **account root** `MBS-/` (one path segment + trailing slash) — this would purge ALL machines under that account. Override only with the extra `--allow-account-root` flag (NOT used by default — purge machine-level `CBB_` prefixes instead). A valid machine target looks like `MBS-/CBB_/`. The command also **warns** (does not fail) when a prefix lacks a trailing `/`, since a slash-less prefix can match a sibling whose name merely starts with the same characters. Without `--confirm`, `delete-prefix` prints the exact rule(s) it WOULD add and a prominent `[WARNING]` about irreversible deletion, then exits non-zero (3) — matching the other gated commands. `lifecycle-remove` is similarly gated (removing a rule is far less dangerous than adding one, but gated for consistency); without `--confirm` it lists which existing rules it would remove and exits 3. ### Cleanup workflow `delete-prefix` -> wait ~24-48h for B2's lifecycle pass -> `bucket-size` to verify the stored size dropped -> `lifecycle-remove` to strip the spent purge rule. Leaving the rule in place is harmless (everything under the prefix is already gone) but tidy buckets are easier to reason about. ## Storage cost ACG's Backblaze B2 cost basis is **$0.00695 per GB** stored, defined as `RATE_PER_GB_USD` in `scripts/b2_client.py` (recorded in `.claude/memory/reference_backblaze_storage_rate.md`) and used by the GuruRMM mspbackups storage-cost calc. Override at the CLI with `--rate`. GB is **decimal** (`bytes / 1e9`), matching how storage providers bill — NOT 2^30. Cost = `GB * rate`. Size MUST be summed over **all file versions** (`b2_list_file_versions`, `action == "upload"`), not just the latest names, because B2 bills every stored version. `bucket-size` and `usage` do this automatically. For very large buckets this issues many list transactions — `usage` prints a `[WARNING]` to that effect. ## Account structure (for context) - accountId `46f69bc61163`, region `us-west-001`. - 12 buckets (all `allPrivate`), mostly per-client MSP360/CloudBerry backup destinations with keys like `MBS-/CBB_/...`: ACG-BST, ACG-Brett, ACG-Dataforth, ACG-GLAZTECH, ACG-IX, ACG-Internal, ACG-Lens, ACG-PST, ACG-REDNOUR, Horseshoe, MSPBackups20200311, VWP-Backup. `usage` derives a client label by stripping the `ACG-` prefix. - 2 application keys: `cloudberrykey` (the MSP360/CloudBerry key) and `ClaudeTools` (the key this skill uses). ## Reference Full verified v3 auth flow, every method used with request/response shape, the file-version billing note, pagination, the error shape, and the cost formula: `references/api-reference.md`.