openclaw-backup-optimized
Optimized OpenClaw backup skill for creating full snapshots with workspace archive splitting, change summaries, restore instructions, and Discord notifications. Use when you need to set up or run automated backups, configure backup cron jobs, or document/restore OpenClaw state. Triggers on backup automation, backup scripts, snapshot/restore, or GitHub backup repos.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install openclaw-skills-openclaw-backup-optimized
Repository
Skill path: skills/cccarv82/openclaw-backup-optimized
Optimized OpenClaw backup skill for creating full snapshots with workspace archive splitting, change summaries, restore instructions, and Discord notifications. Use when you need to set up or run automated backups, configure backup cron jobs, or document/restore OpenClaw state. Triggers on backup automation, backup scripts, snapshot/restore, or GitHub backup repos.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: openclaw.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install openclaw-backup-optimized into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/openclaw/skills before adding openclaw-backup-optimized to shared team environments
- Use openclaw-backup-optimized for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: openclaw-backup-optimized
description: "Optimized OpenClaw backup skill for creating full snapshots with workspace archive splitting, change summaries, restore instructions, and Discord notifications. Use when you need to set up or run automated backups, configure backup cron jobs, or document/restore OpenClaw state. Triggers on backup automation, backup scripts, snapshot/restore, or GitHub backup repos."
---
# OpenClaw Backup (Optimized)
## What this skill does
Use this skill to **install, configure, and run** the optimized OpenClaw backup workflow:
- Full snapshot of `~/.openclaw`
- Workspace archive split into ~90MB parts + SHA256
- Rich Discord notification (summary + restore steps)
- Retention of last N reports
## Files
- Script: `scripts/backup.js` (cross-platform)
- Reference config: `references/CONFIG.md`
## Install / Setup
1) Copy the script into your tools folder:
```bash
cp scripts/backup.js ~/.openclaw/workspace/tools/openclaw-backup.js
```
2) Configure env vars (see references/CONFIG.md):
**macOS/Linux (bash/zsh):**
```bash
export OPENCLAW_HOME="$HOME/.openclaw"
export OPENCLAW_BACKUP_DIR="$HOME/.openclaw-backup"
export BACKUP_REPO_URL="https://github.com/your/repo.git"
export BACKUP_CHANNEL_ID="1234567890"
export BACKUP_TZ="America/Sao_Paulo"
export BACKUP_MAX_HISTORY=7
```
**Windows (PowerShell):**
```powershell
$env:OPENCLAW_HOME="$env:USERPROFILE\.openclaw"
$env:OPENCLAW_BACKUP_DIR="$env:USERPROFILE\.openclaw-backup"
$env:BACKUP_REPO_URL="https://github.com/your/repo.git"
$env:BACKUP_CHANNEL_ID="1234567890"
$env:BACKUP_TZ="America/Sao_Paulo"
$env:BACKUP_MAX_HISTORY="7"
```
3) Run once:
```bash
node ~/.openclaw/workspace/tools/openclaw-backup.js
```
4) Create cron (OpenClaw cron runs in the gateway environment):
```bash
openclaw cron add --name "openclaw-backup-daily" \
--cron "0 5,10,15,20 * * *" --tz "America/Sao_Paulo" \
--exec "node ~/.openclaw/workspace/tools/openclaw-backup.js"
```
## Restore
Use the restore instructions printed in the backup notification.
## Notes
- Excludes noisy session lock/deleted files for smaller diffs.
- Requires `git` and `node` (>=18).
- Uses `openclaw message send` for notifications (no webhook).
- `scripts/openclaw-backup.sh` is legacy (Linux/macOS) and will be removed; use `backup.js`.
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### scripts/backup.js
```javascript
#!/usr/bin/env node
const fs = require('fs');
const fsp = fs.promises;
const path = require('path');
const crypto = require('crypto');
const { spawnSync } = require('child_process');
const tar = require('tar');
const minimatch = require('minimatch');
const HOME = process.env.HOME || process.env.USERPROFILE || '.';
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(HOME, '.openclaw');
const OPENCLAW_BACKUP_DIR = process.env.OPENCLAW_BACKUP_DIR || path.join(HOME, '.openclaw-backup');
const BACKUP_REPO_URL = process.env.BACKUP_REPO_URL || '';
const BACKUP_CHANNEL_ID = process.env.BACKUP_CHANNEL_ID || '';
const BACKUP_TZ = process.env.BACKUP_TZ || 'America/Sao_Paulo';
const BACKUP_MAX_HISTORY = Number(process.env.BACKUP_MAX_HISTORY || 7);
const BACKUP_SPLIT_SIZE_MB = Number(process.env.BACKUP_SPLIT_SIZE_MB || 90);
const EXCLUDES = [
'.openclaw-backup/**',
'workspace/**',
'media/inbound/**',
'agents/main/sessions/*.jsonl.lock',
'agents/main/sessions/*.jsonl.deleted.*'
];
const formatDateTime = (tz) => {
const parts = new Intl.DateTimeFormat('sv-SE', {
timeZone: tz,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
}).formatToParts(new Date());
const map = Object.fromEntries(parts.map((p) => [p.type, p.value]));
return `${map.year}-${map.month}-${map.day} ${map.hour}:${map.minute}`;
};
const formatShort = (tz) => {
const parts = new Intl.DateTimeFormat('sv-SE', {
timeZone: tz,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
}).formatToParts(new Date());
const map = Object.fromEntries(parts.map((p) => [p.type, p.value]));
return `${map.year}${map.month}${map.day}-${map.hour}${map.minute}`;
};
const toPosix = (p) => p.split(path.sep).join('/');
const isExcluded = (rel) => {
const posix = toPosix(rel);
return EXCLUDES.some((pattern) => minimatch(posix, pattern, { dot: true }));
};
const notify = (title, body) => {
const payload = `**${title}**\n${body}`;
console.log(payload);
if (!BACKUP_CHANNEL_ID) return;
const res = spawnSync('openclaw', ['message', 'send', '--channel', 'discord', '--target', BACKUP_CHANNEL_ID, '--message', payload], {
stdio: 'ignore'
});
if (res.error) {
console.error('notify error:', res.error.message);
}
};
const onError = (nowTs) => {
notify('❌ Backup failed', `Time: ${nowTs} (${BACKUP_TZ})\nCheck server logs for details.`);
};
const ensureDir = async (dir) => {
await fsp.mkdir(dir, { recursive: true });
};
const moveHistoryOut = async (dest) => {
const history = path.join(dest, 'history');
try {
await fsp.access(history);
} catch {
return null;
}
const tmp = path.join(dest, `._history_${Date.now()}`);
await fsp.rename(history, tmp);
return tmp;
};
const restoreHistory = async (dest, tmp) => {
if (!tmp) return;
const history = path.join(dest, 'history');
await fsp.rename(tmp, history);
};
const syncDir = async (src, dest) => {
const historyTmp = await moveHistoryOut(dest).catch(() => null);
await fsp.rm(dest, { recursive: true, force: true });
await ensureDir(dest);
if (historyTmp) await restoreHistory(dest, historyTmp);
const walk = async (current) => {
const entries = await fsp.readdir(current, { withFileTypes: true });
for (const entry of entries) {
const full = path.join(current, entry.name);
const rel = path.relative(src, full);
if (isExcluded(rel)) continue;
const target = path.join(dest, rel);
if (entry.isDirectory()) {
await ensureDir(target);
await walk(full);
} else if (entry.isFile()) {
await ensureDir(path.dirname(target));
await fsp.copyFile(full, target);
}
}
};
await walk(src);
};
const listWorkspaceEntries = async (dir) => {
const entries = [];
const walk = async (current) => {
const items = await fsp.readdir(current, { withFileTypes: true });
for (const item of items) {
const full = path.join(current, item.name);
const rel = path.relative(dir, full);
if (item.isDirectory()) {
await walk(full);
} else if (item.isFile()) {
const stat = await fsp.stat(full);
entries.push(`${toPosix(rel)}|${stat.size}|${stat.mtimeMs}`);
}
}
};
await walk(dir);
return entries;
};
const hashWorkspace = async (workspaceDir) => {
const entries = await listWorkspaceEntries(workspaceDir);
entries.sort();
const hash = crypto.createHash('sha256');
for (const line of entries) hash.update(line + '\n');
return hash.digest('hex');
};
const splitFile = async (filePath, outPrefix, splitSizeBytes) => {
const hash = crypto.createHash('sha256');
const read = fs.createReadStream(filePath);
let partIndex = 0;
let currentSize = 0;
let out = fs.createWriteStream(`${outPrefix}${String(partIndex).padStart(3, '0')}`);
await new Promise((resolve, reject) => {
read.on('data', (chunk) => {
hash.update(chunk);
let offset = 0;
while (offset < chunk.length) {
const remaining = splitSizeBytes - currentSize;
const slice = chunk.subarray(offset, offset + remaining);
out.write(slice);
currentSize += slice.length;
offset += slice.length;
if (currentSize >= splitSizeBytes) {
out.end();
partIndex += 1;
currentSize = 0;
out = fs.createWriteStream(`${outPrefix}${String(partIndex).padStart(3, '0')}`);
}
}
});
read.on('error', reject);
read.on('end', () => {
out.end();
resolve();
});
});
return hash.digest('hex');
};
const git = (args, cwd) => {
const res = spawnSync('git', args, { cwd, stdio: 'pipe', encoding: 'utf8' });
if (res.error) throw res.error;
return res.stdout.trim();
};
const main = async () => {
const nowTs = formatDateTime(BACKUP_TZ);
const shortTs = formatShort(BACKUP_TZ);
const isoTs = new Date().toISOString();
try {
await ensureDir(OPENCLAW_BACKUP_DIR);
await syncDir(OPENCLAW_HOME, OPENCLAW_BACKUP_DIR);
const workspaceDir = path.join(OPENCLAW_HOME, 'workspace');
const workspaceHashFile = path.join(OPENCLAW_BACKUP_DIR, '.workspace.hash');
const workspaceShaFile = path.join(OPENCLAW_BACKUP_DIR, '.workspace.tar.sha256');
let prevHash = '';
try {
prevHash = (await fsp.readFile(workspaceHashFile, 'utf8')).trim();
} catch {}
let workspaceHash = '';
let workspaceChanged = 0;
let tarSha256 = '';
try {
workspaceHash = await hashWorkspace(workspaceDir);
} catch {}
if (workspaceHash && workspaceHash !== prevHash) {
workspaceChanged = 1;
}
if (workspaceChanged) {
const tarPath = path.join(OPENCLAW_BACKUP_DIR, 'workspace.tar.gz');
const partPrefix = path.join(OPENCLAW_BACKUP_DIR, 'workspace.tar.gz.part.');
const entries = await fsp.readdir(OPENCLAW_BACKUP_DIR);
for (const entry of entries) {
if (entry.startsWith('workspace.tar.gz.part.')) {
await fsp.rm(path.join(OPENCLAW_BACKUP_DIR, entry), { force: true });
}
}
await fsp.rm(tarPath, { force: true });
await tar.c({ gzip: true, cwd: OPENCLAW_HOME, file: tarPath }, ['workspace']);
const splitSize = BACKUP_SPLIT_SIZE_MB * 1024 * 1024;
tarSha256 = await splitFile(tarPath, partPrefix, splitSize);
await fsp.rm(tarPath, { force: true });
await fsp.writeFile(workspaceHashFile, workspaceHash);
await fsp.writeFile(workspaceShaFile, tarSha256);
}
git(['init', '-q'], OPENCLAW_BACKUP_DIR);
git(['branch', '-M', 'main'], OPENCLAW_BACKUP_DIR);
git(['add', '-A'], OPENCLAW_BACKUP_DIR);
const status = git(['status', '--porcelain'], OPENCLAW_BACKUP_DIR).split('\n').filter(Boolean);
const added = status.filter((line) => line.startsWith('A ')).map((line) => line.slice(3));
const modified = status.filter((line) => line.startsWith('M ')).map((line) => line.slice(3));
const deleted = status.filter((line) => line.startsWith('D ')).map((line) => line.slice(3));
const report = {
timestamp: isoTs,
timezone: BACKUP_TZ,
counts: {
added: added.length,
modified: modified.length,
deleted: deleted.length
},
workspace: {
changed: workspaceChanged,
hash: workspaceHash,
tarSha256
}
};
await fsp.writeFile(path.join(OPENCLAW_BACKUP_DIR, 'backup-report.json'), JSON.stringify(report, null, 2));
const comment = 'Backup OK. Full snapshot (config + memory + workspace) to minimize recovery time. Excluded .jsonl.lock and .jsonl.deleted.* session files to reduce noise.';
const phrase = 'You are building something big — and I am here to keep the line steady with you.';
git(['add', '-A'], OPENCLAW_BACKUP_DIR);
try {
git(['commit', '-m', `backup: ${nowTs}`], OPENCLAW_BACKUP_DIR);
} catch {}
const commitSha = git(['rev-parse', '--short', 'HEAD'], OPENCLAW_BACKUP_DIR) || 'unknown';
try {
git(['remote', 'remove', 'origin'], OPENCLAW_BACKUP_DIR);
} catch {}
if (BACKUP_REPO_URL) {
git(['remote', 'add', 'origin', BACKUP_REPO_URL], OPENCLAW_BACKUP_DIR);
git(['push', '-u', 'origin', 'main', '--force'], OPENCLAW_BACKUP_DIR);
}
const historyDir = path.join(OPENCLAW_BACKUP_DIR, 'history', shortTs);
await ensureDir(historyDir);
await fsp.copyFile(path.join(OPENCLAW_BACKUP_DIR, 'backup-report.json'), path.join(historyDir, 'backup-report.json'));
if (workspaceHash) await fsp.copyFile(path.join(OPENCLAW_BACKUP_DIR, '.workspace.hash'), path.join(historyDir, '.workspace.hash'));
if (tarSha256) await fsp.copyFile(path.join(OPENCLAW_BACKUP_DIR, '.workspace.tar.sha256'), path.join(historyDir, '.workspace.tar.sha256'));
const historyRoot = path.join(OPENCLAW_BACKUP_DIR, 'history');
let historyEntries = [];
try {
historyEntries = (await fsp.readdir(historyRoot)).map((name) => path.join(historyRoot, name));
} catch {}
const stats = await Promise.all(historyEntries.map(async (entry) => ({
entry,
stat: await fsp.stat(entry)
})));
stats.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
const excess = stats.slice(BACKUP_MAX_HISTORY);
for (const item of excess) {
await fsp.rm(item.entry, { recursive: true, force: true });
}
let summary = `**Summary**\n• Time: ${nowTs} (${BACKUP_TZ})\n• Commit: ${commitSha}\n• Changes: +${added.length} ~${modified.length} -${deleted.length}\n• Workspace changed: ${workspaceChanged}`;
if (tarSha256) summary += `\n• Workspace tar SHA256: ${tarSha256}`;
const formatList = (label, items) => {
if (!items.length) return '';
const top = items.slice(0, 12);
return `\n\n**${label} (top 12)**\n${top.map((item) => `• ${item}`).join('\n')}`;
};
const changes = [
formatList('Added', added),
formatList('Modified', modified),
formatList('Deleted', deleted)
].join('');
const restoreNote = `\n\n**How to restore this backup**\n1) openclaw gateway stop\n2) Rename current OPENCLAW_HOME (e.g., mv ${OPENCLAW_HOME} ${OPENCLAW_HOME}-restore-${shortTs})\n3) git clone ${BACKUP_REPO_URL || '<repo>'} backup\n4) cd backup && git checkout ${commitSha}\n5) Create OPENCLAW_HOME and copy files back\n6) Merge workspace parts (cat workspace.tar.gz.part.* > workspace.tar.gz) and extract with tar\n7) openclaw gateway start\n\nNote: On Windows, use PowerShell (Move-Item/Copy-Item/tar.exe) for the steps above.`;
notify('✅ Backup completed', `${summary}${changes}\n\n**Comment**\n${comment}\n\n**Phrase**\n${phrase}${restoreNote}`);
} catch (err) {
onError(formatDateTime(BACKUP_TZ));
console.error(err);
process.exit(1);
}
};
main();
```
### references/CONFIG.md
```markdown
# Backup skill configuration
## Environment variables
- `OPENCLAW_HOME`: OpenClaw home path (default `~/.openclaw`)
- `OPENCLAW_BACKUP_DIR`: backup destination (default `~/.openclaw-backup`)
- `BACKUP_REPO_URL`: git repo for push (e.g., https://github.com/cccarv82/openclaw-backup.git)
- `BACKUP_CHANNEL_ID`: Discord channel id for notifications (optional)
- `BACKUP_TZ`: timezone (default America/Sao_Paulo)
- `BACKUP_MAX_HISTORY`: local history retention (default 7)
## Requirements
- Node.js >= 18
- git
- tar or zip (for workspace archive)
## Policies
- Excludes `media/inbound`
- Excludes `agents/main/sessions/*.jsonl.lock`
- Excludes `agents/main/sessions/*.jsonl.deleted.*`
- Workspace archive is split into parts (~90MB)
## Suggested cron
0 5,10,15,20 * * * (America/Sao_Paulo)
## Restore
Use the block generated in the backup notification.
```
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### _meta.json
```json
{
"owner": "cccarv82",
"slug": "openclaw-backup-optimized",
"displayName": "Openclaw Backup Optimized",
"latest": {
"version": "1.0.1",
"publishedAt": 1770050692872,
"commit": "https://github.com/clawdbot/skills/commit/e3493d3bb22899e1c4aa626ccac1db9bc4135c7f"
},
"history": []
}
```
### scripts/package.json
```json
{
"name": "openclaw-backup-optimized",
"private": true,
"type": "commonjs",
"version": "1.0.0",
"description": "Cross-platform OpenClaw backup script",
"dependencies": {
"minimatch": "^9.0.5",
"tar": "^6.2.1"
}
}
```