Skill: blacktor-tor/openclaw-git-backup (version 0.1.0)

--- name: openclaw-git-backup description: Create, update, validate, or troubleshoot automated git backup workflows for OpenClaw repositories. Use when a user wants a scheduled commit-and-push backup job, wants commit messages to summarize added or modified or deleted files, needs git credential setup for HTTPS pushes, or needs to debug why an OpenClaw cron backup is not running or not pushing. --- # OpenClaw Git Backup Use this skill to build a reusable nightly git backup workflow for an OpenClaw repo. What this workflow does: - stages repo changes - optionally excludes scheduler noise or other paths - creates a commit only when tracked changes exist - writes a commit subject with `+added ~modified -deleted` counts - writes a commit body listing changed files - still runs `git push` even if there was no new commit, so older unpushed commits are flushed - integrates with OpenClaw cron and should always be force-run once after setup or edits ## Inputs to Confirm Before changing anything, confirm these values: - repo root, e.g. `<repo-root>` - runtime script path, e.g. `<runtime-script>` - timezone, e.g. `<timezone>` - remote name, usually `origin` - branch behavior: current branch or explicit branch - exclude patterns, if any - whether HTTPS token setup is needed If the user already implied these values, proceed without turning it into an interview. ## Defaults Reasonable defaults for a fresh setup: - remote: `origin` - timezone: `UTC` - author name: `OpenClaw Backup` - author email: `backup@local` - branch: current branch, else `main` when detached - exclude patterns: none unless the repo has scheduler self-noise ## Bundled Script This skill includes `scripts/nightly_git_backup.sh`. Prefer using the bundled script as the implementation source, then copy it into the runtime location the user wants the cron job to call. Typical install step: ```bash install -D -m 755 <skill-dir>/scripts/nightly_git_backup.sh <runtime-script> bash -n <runtime-script> ``` ## Configuration Model The bundled script supports these env vars: - `BACKUP_REMOTE` — default `origin` - `BACKUP_BRANCH` — optional explicit target branch - `BACKUP_TZ` — default `UTC` - `BACKUP_AUTHOR_NAME` — default `OpenClaw Backup` - `BACKUP_AUTHOR_EMAIL` — default `backup@local` - `BACKUP_EXCLUDES` — comma-separated git pathspec exclude patterns, e.g. `cron,cron/**,.cache,.cache/**` The first positional argument is the repo path. If omitted, the script uses `BACKUP_REPO` or the current directory. ## Recommended Flow ### 1. Inspect current state Check: - `git -C <repo-root> rev-parse --show-toplevel` - `git -C <repo-root> status --short --branch` - `git -C <repo-root> remote -v` - `openclaw cron list` or equivalent cron listing ### 2. Install or update the runtime script Copy the bundled script into the runtime path the cron job will execute. ### 3. Configure push auth only if needed If HTTPS push is already working, leave auth alone. If the user explicitly provides a GitHub HTTPS token, prefer repo-local credential storage instead of embedding credentials in the remote URL: ```bash git -C <repo-root> config credential.helper 'store --file=<repo-root>/.git/credentials' git -C <repo-root> config credential.useHttpPath true printf 'protocol=https\nhost=github.com\npath=OWNER/REPO.git\nusername=x-access-token\npassword=TOKEN\n' | git -C <repo-root> credential approve chmod 600 <repo-root>/.git/credentials git -C <repo-root> ls-remote --heads <remote> <branch> ``` Rules: - never write the token into the remote URL - never store the real token inside this skill - tighten credential file permissions to `600` ### 4. Create or update the cron job Read `references/cron-job-template.md`. Set the job so it executes: ```bash bash -lc '<runtime-script> <repo-root>' ``` Typical cron settings: - isolated agentTurn - low thinking - timeout 600s - silent delivery unless the user wants alerts ### 5. Force-run once immediately Do not stop at “job created”. Always test the full path. After force-run, check: - latest commit subject - latest commit body - `git status --short --branch` - `git rev-list --left-right --count <remote>/<branch>...HEAD` if push is expected ## Validation Checklist ### Script - `bash -n <runtime-script>` ### Git - `git -C <repo-root> status --short --branch` - `git -C <repo-root> remote -v` - `git -C <repo-root> ls-remote --heads <remote> <branch>` ### Cron - list jobs - force-run the backup job - inspect recent run results ### Expected Outcome - if staged changes exist after exclusions, a new commit is created and pushed - if no new changes exist, push still runs to flush older local commits - exclude patterns prevent scheduler self-noise from generating fake backup commits ## Commit Message Contract The bundled script writes: ### Subject ```text backup: snapshot YYYY-MM-DD HH:MM:SS +0000 | +A ~M -D ``` ### Body ```text Added(n) - ... Modified(n) - ... Deleted(n) - ... Renamed(n) - old -> new ``` Keep the body factual and file-oriented. ## Common Pitfalls ### Scheduler self-noise Some repos contain scheduler state that changes every run. Exclude it explicitly with `BACKUP_EXCLUDES`. Example: ```bash BACKUP_EXCLUDES='cron,cron/**' ``` ### Runtime state outside the repo Git only backs up files inside the target repo. If a workflow also depends on directories outside the repo, they are not covered unless the user explicitly adds a second sync or mirrors them into the repo. See `references/backup-scope.md`. ### Wrong success criteria “Cron job exists” is not enough. The real success condition is: 1. force-run works 2. commit format is correct 3. push succeeds 4. remote catches up ## Output Style When reporting back, keep it concrete: - what was configured - what was tested - whether push worked - what is still outside backup scope

path: references/backup-scope.md
content: # Backup Scope

## What a git backup covers

A git backup only covers files inside the target repository.

Typical included items:

- workspace files already tracked by git
- custom skills and overrides stored inside the repo
- runtime scripts stored inside the repo
- lockfiles or docs stored inside the repo

## What it does not cover by default

These are not backed up unless they are also inside the repo, copied into the repo, or synced separately:

- state directories outside the repo
- caches outside the repo
- local companion folders created by other skills or tools
- credentials stored outside the repo

## Common gotcha

Users often assume “the app folder” and “the runtime state folders” are the same thing. They usually are not.

When documenting a backup flow, explicitly separate:

1. code/config tracked by git
2. runtime state outside git

## Scheduler self-noise

If the scheduler writes its own state inside the repo, exclude it explicitly or the backup job may generate commits from its own bookkeeping.

Examples:

- `cron`
- `cron/**`
- `.cache`
- `.cache/**`

## Promise carefully

Do not claim “everything is backed up” unless you have verified that all required state lives inside the repo or has a second backup path.


path: references/cron-job-template.md
content: # Cron Job Template

Use this reference when wiring the backup into OpenClaw cron.

## Fill these placeholders first

- `<repo-root>`
- `<runtime-script>`
- `<timezone>`
- `<remote>`
- `<branch>`
- `<exclude-patterns>`

Example exclude patterns:

```text
cron,cron/**
```

## Recommended env wrapper

```bash
BACKUP_REMOTE='<remote>' \
BACKUP_BRANCH='<branch>' \
BACKUP_TZ='<timezone>' \
BACKUP_AUTHOR_NAME='OpenClaw Backup' \
BACKUP_AUTHOR_EMAIL='backup@local' \
BACKUP_EXCLUDES='<exclude-patterns>' \
bash -lc '<runtime-script> <repo-root>'
```

## agentTurn message template

```text
You are running a scheduled git backup for <repo-root>. Requirements: 1) commit if there are staged changes after exclusions, 2) use the bundled backup script so the commit subject includes +added ~modified -deleted counts and the commit body lists changed files, 3) push to <remote>/<branch>, 4) still run push even if there was no new commit so older local commits are flushed, 5) do not ask the user questions. First run this exact command, then summarize the result briefly:

BACKUP_REMOTE='<remote>' BACKUP_BRANCH='<branch>' BACKUP_TZ='<timezone>' BACKUP_AUTHOR_NAME='OpenClaw Backup' BACKUP_AUTHOR_EMAIL='backup@local' BACKUP_EXCLUDES='<exclude-patterns>' bash -lc '<runtime-script> <repo-root>'
```

## Suggested cron job shape

- schedule: user preference
- sessionTarget: isolated
- delivery: none unless the user wants alerts
- timeoutSeconds: 600
- thinking: low

## Acceptance check

After add or update:

1. force-run once
2. inspect the latest commit subject and body
3. confirm push succeeded
4. confirm the repo is no longer ahead of the remote, if push was expected


path: scripts/nightly_git_backup.sh
content: #!/usr/bin/env bash
set -euo pipefail

repo="${1:-${BACKUP_REPO:-$(pwd)}}"
remote="${BACKUP_REMOTE:-origin}"
branch_override="${BACKUP_BRANCH:-}"
timezone="${BACKUP_TZ:-UTC}"
author_name="${BACKUP_AUTHOR_NAME:-OpenClaw Backup}"
author_email="${BACKUP_AUTHOR_EMAIL:-backup@local}"
exclude_csv="${BACKUP_EXCLUDES:-}"

if [[ ! -d "$repo/.git" ]]; then
  echo "ERROR: not a git repo: $repo" >&2
  exit 1
fi

cd "$repo"

trim() {
  local s="$1"
  s="${s#${s%%[![:space:]]*}}"
  s="${s%${s##*[![:space:]]}}"
  printf '%s' "$s"
}

stage_changes() {
  local -a exclude_args=()
  if [[ -n "$exclude_csv" ]]; then
    IFS=',' read -r -a raw_excludes <<< "$exclude_csv"
    for item in "${raw_excludes[@]}"; do
      item="$(trim "$item")"
      [[ -z "$item" ]] && continue
      exclude_args+=(":(exclude)$item")
    done
  fi

  git add -A -- . "${exclude_args[@]}"
}

print_section() {
  local title="$1"
  local count="$2"
  local lines="${3:-}"

  printf '%s(%s)\n' "$title" "$count"
  if [[ -n "$lines" ]]; then
    printf '%s\n' "$lines"
  else
    echo "- none"
  fi
}

stage_changes

did_commit=0
added_count=0
modified_count=0
deleted_count=0
renamed_count=0
status_file=""
body_file=""
trap '[[ -n "$status_file" ]] && rm -f "$status_file"; [[ -n "$body_file" ]] && rm -f "$body_file"' EXIT

if ! git diff --cached --quiet; then
  status_file="$(mktemp)"
  body_file="$(mktemp)"
  git diff --cached --name-status --find-renames > "$status_file"

  added_count=$(awk -F '\t' '$1=="A"{c++} END{print c+0}' "$status_file")
  modified_count=$(awk -F '\t' '$1=="M"{c++} END{print c+0}' "$status_file")
  deleted_count=$(awk -F '\t' '$1=="D"{c++} END{print c+0}' "$status_file")
  renamed_count=$(awk -F '\t' '$1 ~ /^R[0-9]*$/{c++} END{print c+0}' "$status_file")

  added_lines=$(awk -F '\t' '$1=="A"{print "- "$2}' "$status_file")
  modified_lines=$(awk -F '\t' '$1=="M"{print "- "$2}' "$status_file")
  deleted_lines=$(awk -F '\t' '$1=="D"{print "- "$2}' "$status_file")
  renamed_lines=$(awk -F '\t' '$1 ~ /^R[0-9]*$/{print "- "$2" -> "$3}' "$status_file")

  {
    print_section "Added" "$added_count" "$added_lines"
    echo
    print_section "Modified" "$modified_count" "$modified_lines"
    echo
    print_section "Deleted" "$deleted_count" "$deleted_lines"
    if [[ "$renamed_count" -gt 0 ]]; then
      echo
      print_section "Renamed" "$renamed_count" "$renamed_lines"
    fi
  } > "$body_file"

  subject="backup: snapshot $(TZ="$timezone" date "+%F %T %z") | +${added_count} ~${modified_count} -${deleted_count}"
  GIT_AUTHOR_NAME="$author_name" \
  GIT_AUTHOR_EMAIL="$author_email" \
  GIT_COMMITTER_NAME="$author_name" \
  GIT_COMMITTER_EMAIL="$author_email" \
    git commit -m "$subject" -m "$(cat "$body_file")"
  did_commit=1
fi

branch="$branch_override"
if [[ -z "$branch" ]]; then
  branch="$(git rev-parse --abbrev-ref HEAD)"
  if [[ -z "$branch" || "$branch" == "HEAD" ]]; then
    branch="main"
  fi
fi

git push --porcelain "$remote" "HEAD:${branch}"

echo "DID_COMMIT=$did_commit"
echo "HEAD=$(git rev-parse --short HEAD)"
echo "BRANCH=$branch"
git status --short --branch


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