Skill: servicosmkt/backup-to-r2 (version 1.1.1)

--- name: openclaw-backup-r2 description: Set up and run complete, encrypted, off-site backups of an OpenClaw install to Cloudflare R2 via restic, plus portable restore. Use when the user wants to back up, protect, restore, or migrate their OpenClaw config, agents, and credentials. --- # OpenClaw Backup → Cloudflare R2 (via restic) This skill helps the user create **complete, encrypted, off-site backups** of their OpenClaw installation and restore them on any machine. It backs up everything OpenClaw's built-in `.bak` misses: credentials, agents, workspace, and cron — not just `openclaw.json`. ## When to use - The user wants to back up their OpenClaw setup (config, agents, credentials). - The user lost agents/config (e.g., after `openclaw doctor --fix`) and wants protection going forward. - The user wants to restore a backup or migrate OpenClaw to a new PC. - The user wants automatic daily backups. ## What it backs up `openclaw backup create --verify` produces a verified `.tar.gz` containing config, credentials, agents, workspace, and cron. `restic` then encrypts and deduplicates it locally and uploads to Cloudflare R2. Retention keeps 7 daily + 4 weekly + 6 monthly snapshots. ## Prerequisites (guide the user to set these up) 1. **restic** installed (on PATH or at `./restic/restic.exe`). 2. A **Cloudflare R2** bucket + S3 API token. 3. Copy `.env.example` → `.env` and fill `RESTIC_REPOSITORY`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`. 4. Create `.restic-pass` with a strong password. **Critical:** this is the only key to the encrypted backups — store it safely and separately. Without it, restore is impossible. 5. Initialize once: `restic init`. > Never commit `.env` or `.restic-pass`. They are git-ignored. Never print their contents. ## Running a backup ```powershell .\backup.ps1 ``` This runs `openclaw backup create --verify`, uploads via restic to R2, prunes old snapshots, and removes the local tar. Logs go to `logs/`. ## Testing a restore (safe, non-destructive) ```powershell .\test-restore.ps1 ``` Downloads the latest snapshot, extracts it, and validates with `openclaw backup verify`. Does **not** touch the live `.openclaw`. ## Restoring (any PC) — ⚠️ destructive Copy the `portable/` folder (with `restic/restic.exe`, `.env`, `.restic-pass`) to the target machine and run: ```powershell .\portable\restore-portable.ps1 ``` It stops the gateway, pulls the latest backup from R2, and installs to `%USERPROFILE%\.openclaw`. > ⚠️ **This OVERWRITES the live `.openclaw`.** Anything created after the last backup is lost. The script requires the user to type `RESTAURAR` to confirm and moves the current folder to `.openclaw.backup-<date>` first (this local backup can fail if files are locked, in which case it overwrites in place). **Always have the user run `test-restore.ps1` first** to inspect the backup non-destructively, and only run the real restore when they accept replacing the active install. `$env:OPENCLAW_RESTORE_YES = 1` skips the prompt for automation — never set it on the user's behalf without explicit consent. ## Automating daily backups (Windows) Register a Task Scheduler job that runs `backup.ps1` daily with `-StartWhenAvailable` so it catches up if the PC was off. See the README for the exact `Register-ScheduledTask` command. ## Safety notes - Always confirm the user has saved their `.restic-pass` somewhere safe before relying on backups. It is the only key — losing it makes every backup unrecoverable. - When helping configure, never echo secret values; have the user paste them into `.env`/`.restic-pass` themselves. - This uploads OpenClaw secrets (credentials/auth profiles) to a third-party bucket. Encryption is client-side, but its strength depends entirely on the password. Make sure the user uses a strong passphrase, keeps the R2 bucket private, and never shares the `portable/` folder (it bundles both the R2 credentials and the decryption key). - The restore normally preserves the existing `.openclaw` as a timestamped backup (so it is reversible) — but that safety copy can fail if files are locked. Recommend `test-restore.ps1` before any real restore.

path: backup.ps1
content: $ErrorActionPreference = 'Stop'

# ============================================================
# OpenClaw Backup -> Cloudflare R2 (via restic)
# Gera o tar.gz nativo do OpenClaw, envia pro R2 criptografado
# e deduplicado pelo restic, e aplica retencao automatica.
#
# Requisitos (mesma pasta deste script):
#   .env          -> credenciais R2 + RESTIC_REPOSITORY (veja .env.example)
#   .restic-pass  -> senha de criptografia do repositorio restic
# E o restic instalado (no PATH ou em .\restic\restic.exe)
# ============================================================

$BaseDir   = $PSScriptRoot
$TempDir   = Join-Path $BaseDir 'temp'
$LogDir    = Join-Path $BaseDir 'logs'
$Timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$LogFile   = Join-Path $LogDir ("backup-" + (Get-Date -Format 'yyyyMMdd') + '.log')

function Log([string]$m) {
  $line = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $m"
  Add-Content -Path $LogFile -Value $line
  Write-Host $line
}

# --- localiza restic (PATH ou .\restic\restic.exe) ---
$ResticExe = (Get-Command restic -ErrorAction SilentlyContinue).Source
if (-not $ResticExe) { $ResticExe = Join-Path $BaseDir 'restic\restic.exe' }
if (-not (Test-Path $ResticExe)) { throw "restic nao encontrado. Instale-o ou coloque em .\restic\restic.exe" }

# --- localiza openclaw (PATH ou openclaw.cmd no npm global) ---
$OpenClaw = (Get-Command openclaw -ErrorAction SilentlyContinue).Source
if (-not $OpenClaw) { $OpenClaw = Join-Path $env:APPDATA 'npm\openclaw.cmd' }
if (-not (Test-Path $OpenClaw)) { throw "openclaw nao encontrado. Instale o OpenClaw primeiro." }

# --- carrega credenciais R2 do .env ---
$EnvFile = Join-Path $BaseDir '.env'
if (-not (Test-Path $EnvFile)) { throw "Faltando .env (copie de .env.example e preencha)." }
Get-Content $EnvFile | ForEach-Object {
  $line = $_.Trim()
  if (-not $line -or $line.StartsWith('#')) { return }
  $name, $value = $line.split('=', 2)
  if ($name) { Set-Item -Path "env:$($name.Trim())" -Value $value.Trim() }
}
$PassFile = Join-Path $BaseDir '.restic-pass'
if (-not (Test-Path $PassFile)) { throw "Faltando .restic-pass (senha do repositorio restic)." }
Set-Item -Path 'env:RESTIC_PASSWORD_FILE' -Value $PassFile

# --- normaliza RESTIC_REPOSITORY pro formato S3 do restic ---
if ($env:RESTIC_REPOSITORY -and -not ($env:RESTIC_REPOSITORY -match '^s3:')) {
  if ($env:RESTIC_REPOSITORY -match '^https?://') {
    $env:RESTIC_REPOSITORY = 's3:' + $env:RESTIC_REPOSITORY.TrimEnd('/')
  }
}

New-Item -ItemType Directory -Path $TempDir -Force | Out-Null
New-Item -ItemType Directory -Path $LogDir  -Force | Out-Null

Log "=== Backup iniciado ($Timestamp) ==="

$tarPath = $null
try {
  # 1) tar.gz nativo do OpenClaw + verify
  Log '[1/4] openclaw backup create --verify'
  & $OpenClaw backup create --verify --output $TempDir 2>&1 | Tee-Object -FilePath $LogFile -Append | Out-Null
  if ($LASTEXITCODE -ne 0) { throw "openclaw backup falhou (exit $LASTEXITCODE)" }

  $new = Get-ChildItem $TempDir -Filter '*-openclaw-backup.tar.gz' | Sort-Object LastWriteTime -Descending | Select-Object -First 1
  if (-not $new) { throw "Nao encontrei o .tar.gz gerado em $TempDir" }
  $tarPath = $new.FullName
  Log "Tar gerado: $tarPath ($([math]::Round($new.Length/1MB,2)) MB)"

  # 2) restic backup (dedupe + criptografia client-side)
  Log '[2/4] restic backup -> R2'
  & $ResticExe backup $tarPath --tag openclaw --tag "host:$env:COMPUTERNAME" --tag "date:$Timestamp" 2>&1 | Tee-Object -FilePath $LogFile -Append | Out-Null
  if ($LASTEXITCODE -ne 0) { throw "restic backup falhou (exit $LASTEXITCODE)" }

  # 3) retencao: 7 diarios, 4 semanais, 6 mensais
  Log '[3/4] restic forget --prune'
  & $ResticExe forget --prune --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --tag openclaw 2>&1 | Tee-Object -FilePath $LogFile -Append | Out-Null
  if ($LASTEXITCODE -ne 0) { throw "restic forget falhou (exit $LASTEXITCODE)" }

  # 4) limpa tar local (ja esta no R2 criptografado)
  Log '[4/4] Limpando tar.gz local'
  Remove-Item $tarPath -Force

  Log '=== Backup concluido com sucesso ==='
}
catch {
  Log ("ERRO: " + $_.Exception.Message)
  if ($_.ScriptStackTrace) { Log ("Stack: " + $_.ScriptStackTrace) }
  exit 1
}


path: portable/restore-portable.ps1
content: $ErrorActionPreference = 'Stop'

# ============================================================
# Restore PORTATIL do OpenClaw a partir do repositorio restic
# no Cloudflare R2. Funciona em QUALQUER PC.
#
# Coloque nesta mesma pasta:
#   restic\restic.exe   (binario do restic)
#   .env                (credenciais R2 + RESTIC_REPOSITORY)
#   .restic-pass        (senha do repositorio)
#
# O script: baixa o backup mais recente do R2, extrai e
# instala em %USERPROFILE%\.openclaw, preservando a pasta
# atual como .openclaw.backup-<data> antes de sobrescrever.
# ============================================================

$Root = Split-Path -Parent $MyInvocation.MyCommand.Path
$ResticExe = Join-Path $Root 'restic\restic.exe'
$EnvFile = Join-Path $Root '.env'
$PassFile = Join-Path $Root '.restic-pass'

if (!(Test-Path $ResticExe)) { throw "Missing: $ResticExe" }
if (!(Test-Path $EnvFile)) { throw "Missing: $EnvFile" }
if (!(Test-Path $PassFile)) { throw "Missing: $PassFile" }

Get-Content $EnvFile | ForEach-Object {
  $line = $_.Trim()
  if (!$line -or $line.StartsWith('#')) { return }
  $n,$v = $line.split('=',2)
  if ($n) { Set-Item -Path "env:$($n.Trim())" -Value $v.Trim() }
}
$env:RESTIC_PASSWORD_FILE = $PassFile

if ($env:RESTIC_REPOSITORY -and -not ($env:RESTIC_REPOSITORY -match '^s3:')) {
  if ($env:RESTIC_REPOSITORY -match '^https?://') {
    $env:RESTIC_REPOSITORY = 's3:' + $env:RESTIC_REPOSITORY.TrimEnd('/')
  }
}

$Dest = Join-Path $env:USERPROFILE '.openclaw'

# ============================================================
# AVISO: operacao DESTRUTIVA.
# Este script SUBSTITUI o seu .openclaw ATIVO pelo conteudo do
# backup mais recente do R2. Qualquer alteracao feita DEPOIS do
# ultimo backup (agentes, credenciais, config) sera perdida.
# A pasta atual e movida para .openclaw.backup-<data> antes de
# sobrescrever, mas se essa pasta estiver com arquivos travados
# (gateway aberto, por ex.) o backup local pode falhar e a
# sobrescrita acontece "in place".
# ============================================================
if (Test-Path $Dest) {
  Write-Host ""
  Write-Host "ATENCAO: ja existe um OpenClaw instalado em:" -ForegroundColor Yellow
  Write-Host "  $Dest" -ForegroundColor Yellow
  Write-Host "Continuar VAI SUBSTITUIR essa instalacao pelo backup do R2." -ForegroundColor Red
  Write-Host "Sua pasta atual sera preservada como .openclaw.backup-<data> (se nao estiver travada)." -ForegroundColor Yellow
  Write-Host ""
  if (-not $env:OPENCLAW_RESTORE_YES) {
    $answer = Read-Host "Digite EXATAMENTE 'RESTAURAR' para confirmar (qualquer outra coisa cancela)"
    if ($answer -ne 'RESTAURAR') {
      Write-Host "Cancelado pelo usuario. Nada foi alterado." -ForegroundColor Cyan
      exit 1
    }
  } else {
    Write-Host "OPENCLAW_RESTORE_YES definido: pulando confirmacao (modo nao-interativo)." -ForegroundColor Yellow
  }
}

# para o gateway se o openclaw estiver instalado (reduz lock)
if (Get-Command openclaw -ErrorAction SilentlyContinue) {
  try { & openclaw gateway stop | Out-Null } catch {}
}

$DesktopRestoreRoot = Join-Path ([Environment]::GetFolderPath('Desktop')) 'OpenClaw-restore'
$TmpRestoreRoot = Join-Path $DesktopRestoreRoot 'tmp-restore'
$runId = (Get-Date -Format 'yyyyMMdd-HHmmss')
$RestoreDir = Join-Path $TmpRestoreRoot ('restore-' + $runId)
$UnpackedDir = Join-Path $TmpRestoreRoot ('unpacked-' + $runId)
New-Item -ItemType Directory -Path $RestoreDir -Force | Out-Null
New-Item -ItemType Directory -Path $UnpackedDir -Force | Out-Null

Write-Host "Restic check..." -ForegroundColor Cyan
& $ResticExe check

Write-Host "Restic restore latest (tag openclaw)..." -ForegroundColor Cyan
& $ResticExe restore latest --tag openclaw --target $RestoreDir

$tar = Get-ChildItem $RestoreDir -Recurse -Filter '*-openclaw-backup.tar.gz' | Select-Object -First 1
if (-not $tar) { throw "Could not find *-openclaw-backup.tar.gz under $RestoreDir" }
Write-Host "Tar found: $($tar.FullName)" -ForegroundColor Green

Write-Host "Extracting tar..." -ForegroundColor Cyan
$oldEAP = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
& tar -xzf $tar.FullName -C $UnpackedDir 2>&1 | Out-Null
$ErrorActionPreference = $oldEAP

$found = Get-ChildItem -Path $UnpackedDir -Directory -Filter '.openclaw' -Recurse | Select-Object -First 1
if (-not $found) { throw "Could not find restored .openclaw under $UnpackedDir" }
Write-Host "Restored .openclaw at: $($found.FullName)" -ForegroundColor Green

if (Test-Path $Dest) {
  $BackupDest = $Dest + '.backup-' + (Get-Date -Format 'yyyyMMdd-HHmmss')
  Write-Host "Destination exists. Moving current to: $BackupDest" -ForegroundColor Yellow
  try { Move-Item -Path $Dest -Destination $BackupDest -Force } catch { Write-Host "Move failed (locked). Will overwrite in place." -ForegroundColor Yellow }
}

Write-Host "Copying to: $Dest" -ForegroundColor Cyan
try { Copy-Item -Path $found.FullName -Destination $Dest -Recurse -Force }
catch { Write-Host "Copy failed: $($_.Exception.Message)" -ForegroundColor Red; throw }

if (Get-Command openclaw -ErrorAction SilentlyContinue) {
  & openclaw config validate | Out-Null
  & openclaw status | Out-Null
  Write-Host "OK: openclaw looks healthy." -ForegroundColor Green
} else {
  Write-Host "Done: OpenClaw nao instalado neste PC; pulei a validacao." -ForegroundColor Yellow
}

Write-Host "Restore completed successfully." -ForegroundColor Green


path: README.md
content: # OpenClaw Backup → Cloudflare R2 (via restic)

> **Never lose your OpenClaw agents and config again.**

A complete, encrypted, off-site backup for your [OpenClaw](https://openclaw.ai) setup — pushed to Cloudflare R2 with [restic](https://restic.net). Set it once, and a daily job keeps a versioned, encrypted copy of **everything** in the cloud. If your machine dies, an update breaks your config, or you just want to move to a new PC, you restore the whole thing in minutes.

---

## Why this exists

OpenClaw is powerful, but its config is easy to break. One unlucky `openclaw doctor --fix` can wipe your agents and corrupt `openclaw.json` — and suddenly you're rebuilding credentials, agents, routing rules and channel bindings by hand. That's hours of work to get back to where you were.

This project was born from exactly that pain. The fix is simple: **automated, complete, off-site backups** that you can restore from in minutes — on any machine.

OpenClaw already keeps a local `.bak` of `openclaw.json`, but that only covers the config file. It does **not** cover your credentials, agents, workspace or cron jobs — and it lives on the same disk that might fail. This tool covers all of it, encrypted and off-site.

| | OpenClaw's built-in `.bak` | **This project** |
|---|:---:|:---:|
| `openclaw.json` config | ✅ | ✅ |
| Credentials / auth profiles | ❌ | ✅ |
| Agents & workspaces | ❌ | ✅ |
| Cron / scheduled tasks | ❌ | ✅ |
| Off-site (survives disk failure) | ❌ | ✅ |
| Encrypted | ❌ | ✅ |
| Automatic daily + retention | ❌ | ✅ |
| Restore on a brand-new PC | ❌ | ✅ |

---

## When it saves you

- 💥 **A config change broke everything** → restore yesterday's working state in minutes.
- 🖥️ **Your PC died / you got a new one** → run the portable restore on the new machine, pull from R2, done.
- 🔄 **You're migrating setups** → carry the whole OpenClaw state across machines.
- 😌 **Peace of mind** → a fresh encrypted snapshot lands in the cloud every day, automatically.

---

## How it works

```
openclaw backup create --verify   →   <date>-openclaw-backup.tar.gz
                                            │
                                   restic backup (dedupe + client-side encryption)
                                            │
                                            ▼
                                   Cloudflare R2  (only sees encrypted blobs)
                                            │
                                   restic forget --prune  (retention)
```

1. Generates OpenClaw's **native verified backup** (`openclaw backup create --verify`) — config, credentials, agents, workspace, cron.
2. **restic** encrypts and deduplicates it **on your machine**, then uploads to R2. The cloud never sees plaintext.
3. Applies **retention**: keeps 7 daily + 4 weekly + 6 monthly snapshots automatically.

### Why restic + R2?

- **End-to-end encryption** — data is encrypted *before* it leaves your machine. R2 only stores ciphertext.
- **Deduplication** — only changed chunks are uploaded, so daily backups are tiny and fast.
- **Cheap & off-site** — Cloudflare R2 has a generous free tier and **no egress fees**, so restores cost nothing.
- **Portable** — restic is a single binary; restore from any computer.

---

## Requirements

- Windows + PowerShell
- [OpenClaw](https://openclaw.ai) installed
- [restic](https://restic.net/#installation) (on your PATH or in `.\restic\restic.exe`)
- A [Cloudflare R2](https://developers.cloudflare.com/r2/) bucket + an S3 API token

---

## Setup

1. **Copy `.env.example` to `.env`** and fill it in:
   ```ini
   RESTIC_REPOSITORY=https://<accountid>.r2.cloudflarestorage.com/<your-bucket>
   AWS_ACCESS_KEY_ID=...
   AWS_SECRET_ACCESS_KEY=...
   ```
2. **Create `.restic-pass`** with a strong password. This encrypts your backups — **store it somewhere safe; without it there is no restore.**
3. **Initialize the restic repo once:**
   ```powershell
   restic init
   ```

---

## Usage

**Back up now:**
```powershell
.\backup.ps1
```
…or just double-click `backup-agora.bat`.

**Test a restore (without touching your live install):**
```powershell
.\test-restore.ps1
```
Downloads the latest backup, extracts it, and validates it with `openclaw backup verify`. The restored folder is left intact for you to inspect.

**Real restore (on any PC) — ⚠️ destructive:**
Copy the `portable\` folder (with `restic\restic.exe`, `.env`, and `.restic-pass`) to the new machine and run:
```powershell
.\portable\restore-portable.ps1
```

> ⚠️ **This OVERWRITES your live `.openclaw`.** The restore replaces `%USERPROFILE%\.openclaw` with the latest backup from R2. **Anything created after the last backup — new agents, credentials, config changes — is lost.** The script asks you to type `RESTAURAR` to confirm, and moves your current folder to `.openclaw.backup-<date>` first. But that local backup can fail if files are locked (e.g. the gateway is running), in which case it overwrites in place. **Run `test-restore.ps1` first** to inspect the backup non-destructively, and only run the real restore when you're sure you want to replace the active install.
>
> Non-interactive automation can skip the prompt by setting `$env:OPENCLAW_RESTORE_YES = 1` — do this only when you fully understand it overwrites without asking.

---

## Automatic daily backup (Windows Task Scheduler)

```powershell
$action   = New-ScheduledTaskAction -Execute "powershell.exe" -Argument '-ExecutionPolicy Bypass -NoProfile -File "C:\path\to\backup.ps1"'
$trigger  = New-ScheduledTaskTrigger -Daily -At 12:00
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Hours 1)
Register-ScheduledTask -TaskName "OpenClaw Backup" -Action $action -Trigger $trigger -Settings $settings -Description "Daily OpenClaw backup to R2"
```
`-StartWhenAvailable` runs the backup as soon as the PC boots if it was off at the scheduled time, so you never miss a day.

---

## Security & privacy

**What leaves your machine:** This tool uploads a complete copy of your OpenClaw install — **including credentials and auth profiles** — to a third-party cloud (Cloudflare R2). Restic encrypts everything *client-side* before upload, so R2 only ever stores ciphertext. But you should understand the trade-offs before relying on it:

- **Your password is the entire security boundary.** Anyone who obtains both the R2 repository *and* your `.restic-pass` can decrypt every secret you ever backed up. A weak password defeats the encryption. Use a long, random passphrase.
- **`.restic-pass` is the only key.** Lose it and your backups are permanently unrecoverable. Back it up **separately** from the repo (a password manager, not the same bucket).
- **Don't share the `portable\` folder loosely.** It bundles `.env` (R2 credentials) and `.restic-pass` (decryption key) together — handing it to someone gives them full access to your backed-up secrets. Treat it like a master key.
- **Retention keeps history.** Old snapshots (7 daily + 4 weekly + 6 monthly) persist in R2, so credentials you rotated or deleted may still live in older backups until they age out.
- `.env`, `.restic-pass`, and restore-test folders are **never** committed (blocked by `.gitignore`). The repo only ships `.env.example` with no real values.
- Make sure your R2 bucket is **private** (no public access) and the S3 API token is scoped to that single bucket.

---

## Contributing

Issues and PRs welcome! This started as a personal fix and is shared so the OpenClaw community doesn't have to relive the "I lost all my agents" nightmare. If you adapt it for Linux/macOS or other S3-compatible storage (Backblaze B2, AWS S3, MinIO), please open a PR.

## License

MIT — use it, fork it, improve it.


path: skill.json
content: {
  "name": "openclaw-backup-r2",
  "version": "1.1.1",
  "summary": "Complete, encrypted, off-site backup of your OpenClaw setup to Cloudflare R2 via restic. Never lose your agents and config again.",
  "description": "Automated daily backup of your entire OpenClaw install (config, credentials, agents, workspace, cron) to Cloudflare R2 using restic. End-to-end encrypted client-side, deduplicated, off-site, with smart retention (7 daily + 4 weekly + 6 monthly) and a portable restore that works on any PC. Born from the pain of a doctor --fix wiping agents and config. Privacy note: this uploads your OpenClaw secrets (credentials/auth profiles) to your own third-party R2 bucket; encryption depends entirely on the strength and safekeeping of your restic password, and the real restore OVERWRITES the live .openclaw install (run test-restore first).",
  "author": "servicosmkt",
  "license": "MIT",
  "keywords": [
    "backup",
    "restore",
    "restic",
    "cloudflare-r2",
    "r2",
    "s3",
    "disaster-recovery",
    "off-site-backup",
    "encrypted-backup",
    "config-backup",
    "agents-backup",
    "credentials",
    "openclaw",
    "automation",
    "scheduled-backup",
    "data-protection",
    "snapshot",
    "deduplication"
  ],
  "homepage": "https://github.com/servicosmkt/openclaw-backup-r2",
  "repository": {
    "type": "git",
    "url": "https://github.com/servicosmkt/openclaw-backup-r2.git"
  },
  "triggers": [
    "backup openclaw",
    "back up my openclaw config",
    "back up my openclaw agents",
    "lost my openclaw agents",
    "lost my openclaw config",
    "openclaw config got wiped",
    "openclaw doctor fix broke my setup",
    "restore openclaw",
    "restore my openclaw backup",
    "set up openclaw backups",
    "automatic openclaw backup",
    "back up openclaw to r2",
    "back up openclaw to cloudflare",
    "migrate openclaw to new pc",
    "protect my openclaw setup"
  ]
}


path: test-restore.ps1
content: $ErrorActionPreference = 'Stop'

# ============================================================
# Testa o restore SEM tocar na sua instalacao do OpenClaw.
# Baixa o backup mais recente do R2, extrai e valida com
# "openclaw backup verify". A pasta restaurada fica intacta
# pra voce inspecionar.
# ============================================================

$BaseDir = $PSScriptRoot

$ResticExe = (Get-Command restic -ErrorAction SilentlyContinue).Source
if (-not $ResticExe) { $ResticExe = Join-Path $BaseDir 'restic\restic.exe' }
if (-not (Test-Path $ResticExe)) { throw "restic nao encontrado." }

$OpenClaw = (Get-Command openclaw -ErrorAction SilentlyContinue).Source
if (-not $OpenClaw) { $OpenClaw = Join-Path $env:APPDATA 'npm\openclaw.cmd' }

# valida arquivos obrigatorios antes de comecar
$EnvFile = Join-Path $BaseDir '.env'
$PassFile = Join-Path $BaseDir '.restic-pass'
if (-not (Test-Path $EnvFile)) { throw "Arquivo nao encontrado: $EnvFile (copie .env.example para .env e preencha)." }
if (-not (Test-Path $PassFile)) { throw "Arquivo nao encontrado: $PassFile (crie com a senha do repositorio restic)." }

# carrega credenciais R2
Get-Content $EnvFile | ForEach-Object {
  $line = $_.Trim()
  if (-not $line -or $line.StartsWith('#')) { return }
  $name, $value = $line.split('=', 2)
  if ($name) { Set-Item -Path "env:$($name.Trim())" -Value $value.Trim() }
}
Set-Item -Path 'env:RESTIC_PASSWORD_FILE' -Value $PassFile
if ($env:RESTIC_REPOSITORY -and -not ($env:RESTIC_REPOSITORY -match '^s3:')) {
  if ($env:RESTIC_REPOSITORY -match '^https?://') {
    $env:RESTIC_REPOSITORY = 's3:' + $env:RESTIC_REPOSITORY.TrimEnd('/')
  }
}

$RestoreDir = Join-Path $BaseDir ('restore-test-' + (Get-Date -Format 'yyyyMMdd-HHmmss'))
New-Item -ItemType Directory -Path $RestoreDir -Force | Out-Null

Write-Host "`n=== 1) restic check ===" -ForegroundColor Cyan
& $ResticExe check
if ($LASTEXITCODE -ne 0) { throw "restic check falhou (exit $LASTEXITCODE) - repositorio inacessivel ou credenciais erradas." }

Write-Host "`n=== 2) snapshots (preview) ===" -ForegroundColor Cyan
& $ResticExe snapshots --tag openclaw
if ($LASTEXITCODE -ne 0) { throw "restic snapshots falhou (exit $LASTEXITCODE)." }

# mesma selecao do restore real (restore-portable.ps1): latest com tag openclaw
Write-Host "`n=== 3) restore latest (tag openclaw) ===" -ForegroundColor Cyan
& $ResticExe restore latest --tag openclaw --target $RestoreDir
if ($LASTEXITCODE -ne 0) { throw "restic restore falhou (exit $LASTEXITCODE)." }

$restoredTar = Get-ChildItem $RestoreDir -Recurse -Filter '*-openclaw-backup.tar.gz' | Select-Object -First 1
if (-not $restoredTar) { Write-Host 'Nao encontrei tar.gz restaurado!' -ForegroundColor Red; exit 1 }
Write-Host "Tar restaurado: $($restoredTar.FullName)" -ForegroundColor Green

Write-Host "`n=== 4) openclaw backup verify (PROVA REAL) ===" -ForegroundColor Cyan
& $OpenClaw backup verify $restoredTar.FullName

if ($LASTEXITCODE -eq 0) {
  Write-Host "`n[OK] RESTORE TESTADO E VALIDADO pelo OPENCLAW" -ForegroundColor Green
  Write-Host "Pasta de teste: $RestoreDir" -ForegroundColor Yellow
  Write-Host "ATENCAO: ela contem seus dados DESCRIPTOGRAFADOS (credenciais em texto puro). Apague-a quando terminar de inspecionar." -ForegroundColor Yellow
} else {
  Write-Host "`n[FALHA] verify retornou erro - investigar!" -ForegroundColor Red
  exit 1
}


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)