Skill: joeyiptk/openclawbackup (version 1.0.0)

--- name: openclaw-backup version: 1.0.0 description: > Backup OpenClaw workspace, config, and state using the built-in `openclaw backup create` command. Also handles cleanup of old backups and health verification. Triggers: - "backup openclaw", "run backup", "backup now" - "check backup health", "verify backup" - "cleanup old backups", "remove old backups" - First run on new machine → run setup + prompt to create cron job Uses built-in `openclaw backup create` — no extra tools needed. Cron message: "backup openclaw" → runs scripts/backup.py --- # openclaw-backup > See [CHANGELOG.md](./CHANGELOG.md) for version history. Automated daily backup skill using OpenClaw's built-in `backup create` command. ## Backup Destination `~/openclaw_backups/` — timestamped `.tar.gz` archives. ## Quick Commands ```bash # Run backup now python3 scripts/backup.py # Run backup with verification python3 scripts/backup.py --verify # Verify latest backup python3 scripts/health_check.py # Cleanup old backups (dry-run, then --execute to delete) python3 scripts/cleanup_old_backups.py --days 90 python3 scripts/cleanup_old_backups.py --days 90 --execute # Setup daily cron at 4 AM python3 scripts/setup_cron.py ``` ## First-Time Setup on New Machine 1. **Test backup:** ```bash cd skills/openclaw-backup python3 scripts/backup.py --verify ``` 2. **Verify health:** ```bash python3 scripts/health_check.py ``` 3. **Setup cron (important — do this!):** ```bash python3 scripts/setup_cron.py ``` This schedules daily 4 AM HKT backup via OpenClaw cron. Without this, backups only run when you manually trigger them. 4. **Verify cron:** ```bash openclaw cron list ``` ## Cron Setup (Already Configured) The daily cron job runs automatically at 04:00 HKT. Trigger message: `backup openclaw` Session: isolated Manage cron: ```bash # List openclaw cron list # Remove openclaw cron remove openclaw-backup:daily # Re-add python3 scripts/setup_cron.py ``` ## What Gets Backed Up - Config files (`~/.openclaw/*.json`) - Credentials (encrypted by OpenClaw) - Session history - Workspace files - Skills and settings ## Retention Backups are kept indefinitely by default. Run cleanup periodically: ```bash # Preview old backups python3 scripts/cleanup_old_backups.py --days 90 # Delete if looks good python3 scripts/cleanup_old_backups.py --days 90 --execute ``` Suggested schedule: quarterly. ## Portable — Copy to New Machine Copy the entire `openclaw-backup/` skill folder to a new machine, then: ```bash cd skills/openclaw-backup python3 scripts/backup.py --verify # test python3 scripts/setup_cron.py # schedule daily python3 scripts/health_check.py # verify ``` No extra dependencies — uses OpenClaw's built-in `backup create`.

path: CHANGELOG.md
content: # Changelog

## 1.0.0 — 2026-04-28
### Added
- Initial release
- Automated daily backup using OpenClaw's built-in `backup create`
- Backups stored in ~/openclaw_backups/ as timestamped .tar.gz archives
- Backup contents: config, credentials, session history, workspace, skills
- Archive verification via `openclaw backup verify`
- Cleanup old backups by age (--days flag, dry-run by default)
- Optional daily cron at 04:00 HKT
- Portable — works on any machine with OpenClaw installed, no extra deps


path: references/SETUP_GUIDE.md
content: # openclaw-backup Setup Guide

Portable setup for any machine running OpenClaw. All paths are auto-detected.

---

## Quick Start (New Machine)

```bash
# 1. Navigate to the skill directory
cd <path-to-openclaw-backup>

# 2. Test backup (creates archive in ~/openclaw_backups)
python3 scripts/backup.py --verify

# 3. Verify the backup was created correctly
python3 scripts/health_check.py

# 4. Setup daily cron at 4 AM HKT — IMPORTANT!
python3 scripts/setup_cron.py

# 5. Verify cron
openclaw cron list
```

Done.

---

## What Each Script Does

| Script | Purpose |
|--------|---------|
| `backup.py` | Run `openclaw backup create` — creates timestamped archive |
| `health_check.py` | Verify archive integrity + list all backups |
| `cleanup_old_backups.py` | Delete old archives (default: dry-run) |
| `setup_cron.py` | Add daily 4 AM cron job via OpenClaw cron |

---

## Script Details

### backup.py

```bash
# Create backup with verification
python3 scripts/backup.py --verify

# Preview only (dry run)
python3 scripts/backup.py --dry-run

# Just create (no verify)
python3 scripts/backup.py
```

Output goes to `~/openclaw_backups/YYYY-MM-DDTHH-MM-SS.000Z-openclaw-backup.tar.gz`

### health_check.py

```bash
# Check latest backup
python3 scripts/health_check.py

# Verify specific archive
python3 scripts/health_check.py --archive /path/to/archive.tar.gz
```

### cleanup_old_backups.py

```bash
# Preview all backups (default dry-run)
python3 scripts/cleanup_old_backups.py

# Preview archives older than 90 days
python3 scripts/cleanup_old_backups.py --days 90

# Actually delete archives older than 90 days
python3 scripts/cleanup_old_backups.py --days 90 --execute
```

### setup_cron.py

```bash
python3 scripts/setup_cron.py
```

Adds cron job:
- Name: `openclaw-backup:daily`
- Time: `0 4 * * *` (04:00 HKT daily)
- Message trigger: `backup openclaw`
- Session: isolated

---

## Cron Setup (Manual)

If you prefer to set up cron manually:

```bash
openclaw cron add \
  --name "openclaw-backup:daily" \
  --message "backup openclaw" \
  --cron "0 4 * * *" \
  --tz "Asia/Hong_Kong" \
  --session isolated \
  --description "Daily OpenClaw backup to ~/openclaw_backups"
```

Remove:
```bash
openclaw cron remove openclaw-backup:daily
```

---

## Path Detection

| Path | Default |
|------|---------|
| Backup directory | `~/openclaw_backups/` |
| OpenClaw state | `~/.openclaw/` (auto-detected by `openclaw backup create`) |

Override backup dir with environment variable:
```bash
export OPENCLAW_BACKUP_DIR=/your/custom/path
```

---

## Portability

Copy the entire `openclaw-backup/` skill folder to a new machine.

On the new machine:
1. Run `python3 scripts/backup.py --verify` to test
2. Run `python3 scripts/setup_cron.py` to schedule daily backup
3. Run `python3 scripts/health_check.py` to verify

Cron jobs must be re-created on each machine (they live in OpenClaw's gateway, not in the skill folder).

---

## Retention Tip

Backups are kept indefinitely by default. Run cleanup quarterly:

```bash
# Preview old backups
python3 scripts/cleanup_old_backups.py --days 90

# Delete if looks good
python3 scripts/cleanup_old_backups.py --days 90 --execute
```

---

## Troubleshooting

### "openclaw: command not found"

OpenClaw CLI is not in PATH. Try:
```bash
which openclaw
# or find it:
find ~ -name "openclaw" -type f 2>/dev/null | head -5
```

### "No backups found"

Run backup first:
```bash
python3 scripts/backup.py --verify
```

### "Permission denied" on backup directory

Create the directory:
```bash
mkdir -p ~/openclaw_backups
chmod 755 ~/openclaw_backups
```

### Cron not running

Check cron status:
```bash
openclaw cron list
```

Manually trigger a backup:
```bash
python3 scripts/backup.py --verify
```

---

## File Structure

```
openclaw-backup/
├── SKILL.md                    ← Skill trigger file
├── SETUP_GUIDE.md              ← This file
└── scripts/
    ├── backup.py               ← Run the backup
    ├── health_check.py         ← Verify backup health
    ├── cleanup_old_backups.py  ← Remove old backups
    └── setup_cron.py           ← Add daily cron
```

path: scripts/backup.py
content: #!/usr/bin/env python3
"""
backup.py — Run OpenClaw backup using built-in `openclaw backup create`.
Supports --verify flag and --dry-run.
"""

import argparse
import os
import subprocess
import sys

DEFAULT_HOME = os.path.expanduser("~")

def get_backup_dir():
    return os.environ.get("OPENCLAW_BACKUP_DIR") or os.path.expanduser(f"{DEFAULT_HOME}/openclaw_backups")


def ensure_backup_dir():
    os.makedirs(get_backup_dir(), exist_ok=True)


def run_backup(verify=False, dry_run=False):
    backup_dir = get_backup_dir()
    ensure_backup_dir()
    cmd = ["openclaw", "backup", "create", "--output", backup_dir]
    if verify:
        cmd.append("--verify")
    if dry_run:
        cmd.append("--dry-run")

    print(f"Running: {' '.join(cmd)}")
    result = subprocess.run(cmd, capture_output=True, text=True)

    print(result.stdout)
    if result.stderr:
        print(result.stderr, file=sys.stderr)

    if result.returncode == 0:
        # Find the created archive path from output
        for line in result.stdout.splitlines():
            if "openclaw-backup.tar.gz" in line or ".tar.gz" in line:
                print(f"✅ Backup created: {line.strip()}")
        return True
    else:
        print(f"❌ Backup failed (exit {result.returncode})")
        return False


def main():
    parser = argparse.ArgumentParser(description="Run OpenClaw backup")
    parser.add_argument("--verify", action="store_true", help="Verify archive after writing")
    parser.add_argument("--dry-run", action="store_true", help="Preview backup plan without writing")
    args = parser.parse_args()

    if args.dry_run:
        print("Dry-run mode — no files will be written.")

    success = run_backup(verify=args.verify, dry_run=args.dry_run)
    sys.exit(0 if success else 1)


if __name__ == "__main__":
    main()

path: scripts/health_check.py
content: #!/usr/bin/env python3
"""
health_check.py — Verify backup archive integrity and report status.
Uses `openclaw backup verify` + checks backup directory.
"""

import argparse
import glob
import os
import subprocess
import sys
from datetime import datetime

DEFAULT_HOME = os.path.expanduser("~")

def get_backup_dir():
    return os.environ.get("OPENCLAW_BACKUP_DIR") or os.path.expanduser(f"{DEFAULT_HOME}/openclaw_backups")


def find_latest_archive():
    backup_dir = get_backup_dir()
    pattern = os.path.join(backup_dir, "*openclaw-backup.tar.gz")
    archives = sorted(glob.glob(pattern), reverse=True)
    if not archives:
        return None
    return archives[0]


def verify_archive(archive_path):
    print(f"Verifying: {archive_path}")
    result = subprocess.run(
        ["openclaw", "backup", "verify", archive_path],
        capture_output=True,
        text=True
    )
    print(result.stdout)
    if result.stderr:
        print(result.stderr, file=sys.stderr)
    return result.returncode == 0


def check_backup_dir():
    backup_dir = get_backup_dir()
    pattern = os.path.join(backup_dir, "*openclaw-backup.tar.gz")
    archives = sorted(glob.glob(pattern), reverse=True)

    if not archives:
        print(f"❌ No backups found in {backup_dir}/")
        return False

    print(f"📁 Backup directory: {backup_dir}")
    print(f"   Total archives: {len(archives)}")
    print()

    # Show all archives with age
    now = datetime.now()
    print("Backups:")
    for i, arch in enumerate(archives[:10]):  # show last 10
        mtime = os.path.getmtime(arch)
        age = now - datetime.fromtimestamp(mtime)
        size_mb = os.path.getsize(arch) / (1024 * 1024)
        marker = " ← latest" if i == 0 else ""
        print(f"   {os.path.basename(arch)}  {size_mb:.1f} MB  {age.days}d old{marker}")

    if len(archives) > 10:
        print(f"   ... and {len(archives) - 10} more")

    return True


def main():
    parser = argparse.ArgumentParser(description="Check backup health")
    parser.add_argument("--archive", help="Specific archive to verify (default: latest)")
    args = parser.parse_args()

    # Check backup directory first
    if not check_backup_dir():
        sys.exit(1)

    print()

    # Verify the archive
    archive = args.archive or find_latest_archive()
    if not archive:
        print("No archive found to verify.")
        sys.exit(1)

    print()
    if verify_archive(archive):
        print("✅ Backup archive is valid")
        sys.exit(0)
    else:
        print("❌ Backup archive verification failed")
        sys.exit(1)


if __name__ == "__main__":
    main()

path: scripts/cleanup_old_backups.py
content: #!/usr/bin/env python3
"""
cleanup_old_backups.py — Remove old backup archives from ~/openclaw_backups/.
Default: dry-run. Use --days to specify age threshold.
"""

import argparse
import glob
import os
import sys
from datetime import datetime, timedelta

DEFAULT_HOME = os.path.expanduser("~")

def get_backup_dir():
    home = os.environ.get("OPENCLAW_BACKUP_DIR") or os.path.expanduser(f"{DEFAULT_HOME}/openclaw_backups")
    return home


def find_archives(days=None):
    backup_dir = get_backup_dir()
    pattern = os.path.join(backup_dir, "*openclaw-backup.tar.gz")
    all_archives = sorted(glob.glob(pattern), reverse=True)
    if days is None:
        return all_archives
    cutoff = datetime.now() - timedelta(days=days)
    old = []
    for arch in all_archives:
        mtime = datetime.fromtimestamp(os.path.getmtime(arch))
        if mtime < cutoff:
            old.append(arch)
    return old


def main():
    parser = argparse.ArgumentParser(description="Cleanup old OpenClaw backups")
    parser.add_argument("--days", type=int, default=None,
                        help="Show backups older than N days (default: show all)")
    parser.add_argument("--dry-run", action="store_true", default=True,
                        help="Preview deletions (default: True)")
    parser.add_argument("--execute", action="store_true",
                        help="Actually delete (overrides dry-run)")
    args = parser.parse_args()

    dry_run = not args.execute

    backup_dir = get_backup_dir()
    all_archives = find_archives(None)
    to_delete = find_archives(args.days) if args.days else []

    if not all_archives:
        print(f"No backups found in {backup_dir}/")
        sys.exit(0)

    print(f"Backup directory: {backup_dir}")
    print(f"Total archives: {len(all_archives)}")

    if args.days:
        print(f"Filter: > {args.days} days old → {len(to_delete)} archive(s) would be affected")

    print()
    print("All backups:")
    now = datetime.now()
    for arch in all_archives:
        mtime = datetime.fromtimestamp(os.path.getmtime(arch))
        age = now - mtime
        size_mb = os.path.getsize(arch) / (1024 * 1024)
        in_delete = arch in to_delete
        marker = " ← TO DELETE" if in_delete else ""
        print(f"   {os.path.basename(arch)}  {size_mb:.1f} MB  {age.days}d old{marker}")

    print()

    if to_delete:
        if dry_run:
            print(f"🗑️  Run with --execute to delete {len(to_delete)} archive(s).")
        else:
            deleted = 0
            for arch in to_delete:
                os.remove(arch)
                deleted += 1
                print(f"   Deleted: {os.path.basename(arch)}")
            print(f"\nDeleted {deleted} archive(s).")
    else:
        if args.days:
            print(f"No archives older than {args.days} days.")


if __name__ == "__main__":
    main()

path: scripts/setup_cron.py
content: #!/usr/bin/env python3
"""
setup_cron.py — Add the daily OpenClaw backup cron job.
Also checks if cron already exists and warns if so.
"""

import subprocess
import sys


def cron_exists(name):
    result = subprocess.run(
        ["openclaw", "cron", "list", "--json"],
        capture_output=True, text=True
    )
    if result.returncode == 0:
        import json
        try:
            data = json.loads(result.stdout)
            crons = data if isinstance(data, list) else data.get("crons", [])
            for c in crons:
                if c.get("name") == name:
                    return True
        except Exception:
            pass
    return False


def add_cron():
    if cron_exists("openclaw-backup:daily"):
        print("⚠️  Cron 'openclaw-backup:daily' already exists.")
        print("   Run 'openclaw cron remove openclaw-backup:daily' to remove it first.")
        return False

    cmd = [
        "openclaw", "cron", "add",
        "--name", "openclaw-backup:daily",
        "--message", "backup openclaw",
        "--cron", "0 4 * * *",
        "--tz", "Asia/Hong_Kong",
        "--session", "isolated",
        "--description", "Daily OpenClaw backup to ~/openclaw_backups"
    ]

    print(f"Running: {' '.join(cmd)}")
    result = subprocess.run(cmd, capture_output=True, text=True)
    print(result.stdout)
    if result.stderr:
        print(result.stderr, file=sys.stderr)
    return result.returncode == 0


def main():
    print("OpenClaw Backup — Cron Setup")
    print("=" * 40)
    print()

    success = add_cron()
    if success:
        print()
        print("✅ Cron job 'openclaw-backup:daily' added.")
        print("   Runs daily at 04:00 HKT.")
        print()
        print("Verify:")
        print("  openclaw cron list")
    else:
        print()
        print("⚠️  Cron was not added. Check output above.")


if __name__ == "__main__":
    main()

path: version.txt
content: 1.0.0


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