Your Pi as a Self-Backing-Up AI Agent — Skills in Git, Scripts on GitHub

11:30 AM · unexpected cron fire
๐Ÿฆž OpenClaw on HAOS · Part 5

Your Pi as a Self-Backing-Up
AI Agent — Skills in Git,
Scripts on GitHub

Every skill, script, and memory file version-controlled in a private GitHub repo. Edit on your PC, push, and the Pi pulls automatically. Midnight auto-backup keeps everything safe. Plus: fixing a cron that fired at the wrong time and staying on the Gemini free tier.

๐Ÿฅง Raspberry Pi 5 · HAOS ๐Ÿ“ฆ GitHub backup · SSH ๐Ÿ”ง Cron bug fixed 0 paid APIs
also fixed: cron fired at wrong time · wakeMode: now · tz defaulted silently ✓ resolved

Automated Backup to GitHub over SSH

A private GitHub repo as the single source of truth for all OpenClaw skills and scripts. Edit on your PC, push to GitHub, Pi pulls automatically. Daily midnight backup from Pi to GitHub. Zero manual file copying.

Repo Structure

clawd-backup/
├── .gitignore
├── MEMORY.md
├── sync-to-github.sh ← Pi → GitHub (midnight cron)
├── pull-from-github.sh ← GitHub → Pi (on demand)
├── memory/
│ └── my-portfolio.json
└── skills/
├── home-assistant/
├── memory/
├── github-sync/
├── portfolio-tracker/
└── safe-exec/
๐Ÿ”’
.gitignore the service account

The Google Sheets service account JSON contains a private key. Even in a private repo, add it to .gitignore and keep a manual backup. One accidental repo visibility change would expose it.

Setup Steps

1
Generate SSH key — store in /config/.ssh (persists across restarts)
mkdir -p /config/.ssh && chmod 700 /config/.ssh
ssh-keygen -t ed25519 -C "openclaw-pi" -f /config/.ssh/id_ed25519 -N ""
cat /config/.ssh/id_ed25519.pub
# Copy this output → GitHub repo → Settings → Deploy keys → Add
# ✅ Check "Allow write access"
๐Ÿ’ก
Why /config/.ssh not ~/.ssh?

In the OpenClaw HAOS container, ~ resolves to /config, not /root. More importantly, /config is HAOS persistent storage — SSH keys survive container restarts. /root/.ssh would be wiped on every restart.

2
Configure git and clone
git config --global user.email "openclaw@pi"
git config --global user.name "OpenClaw Pi"
git config --global core.sshCommand "ssh -i /config/.ssh/id_ed25519"

ssh -T git@github.com -i /config/.ssh/id_ed25519
# Should say: Hi username/repo! You've successfully authenticated

git clone git@github.com:YOUR_USER/YOUR_REPO.git /config/clawd-backup
3
Copy files, create .gitignore, first push
cp -r /config/clawd/skills /config/clawd-backup/
cp -r /config/clawd/memory /config/clawd-backup/
cp /config/clawd/MEMORY.md /config/clawd-backup/

cat > /config/clawd-backup/.gitignore << 'EOF'
memory/gsheets-credentials.json
memory/*.bak.json
__pycache__/
*.pyc
EOF

cd /config/clawd-backup
git add . && git commit -m "initial: openclaw skills + memory"
git push -u origin main
4
Add midnight backup cron
openclaw cron add \
  --name "GitHub Backup Midnight" \
  --cron "0 21 * * *" \
  --tz "Your/Timezone" \
  --wake next-heartbeat \
  --timeout-seconds 120 \
  --session isolated \
  --light-context \
  --announce \
  --to YOUR_TELEGRAM_ID \
  --channel telegram \
  --message "Run: bash /config/clawd-backup/sync-to-github.sh"
# Adjust cron expression to match your local midnight in UTC

The Two Scripts

sync-to-github.sh (Pi → GitHub)
#!/bin/bash
cd /config/clawd-backup
git pull --rebase origin main          # pull PC edits first
cp -r /config/clawd/skills/* skills/
cp -r /config/clawd/memory/* memory/ 2>/dev/null
cp /config/clawd/MEMORY.md .

git diff --quiet && git diff --staged --quiet && exit 0

git add .
git commit -m "auto: $(date '+%Y-%m-%d %H:%M') — scheduled backup"
git push origin main
pull-from-github.sh (GitHub → Pi)
#!/bin/bash
cd /config/clawd-backup
git pull --rebase origin main || exit 1
cp -r skills/* /config/clawd/skills/
cp memory/my-portfolio.json /config/clawd/memory/
cp MEMORY.md /config/clawd/
echo "$(date): Sync complete — active skills updated"

Trigger the pull from Telegram by saying "pull scripts" or "sync from github" — the github-sync skill handles it.

The 11:30 AM Mystery

A Telegram message arrived at 11:30 AM — the cron was supposed to run at 2:30 PM. Three separate problems had stacked on top of each other, each one hiding the next.

Problem 1 — wakeMode: now

OpenClaw's wakeMode: now fires a missed job immediately on container restart instead of waiting for its next scheduled time. The add-on restarted sometime in the morning, saw it hadn't run since yesterday, and fired immediately.

❌ Before
"wakeMode": "now"
// fires immediately on restart
// if last run was missed
✓ After
"wakeMode": "next-heartbeat"
// waits for next heartbeat
// cycle before firing
⚠️
Don't edit jobs.json directly

Manual JSON edits get overwritten when OpenClaw saves its own state. Use the CLI — openclaw cron edit <id> --wake next-heartbeat — so changes persist properly.

Problem 2 — Wrong cron expression

The original expression 30 11 * * 0-4 with your local timezone was always 11:30 AM local time — not 2:30 PM. The intent was 2:30 PM but the expression was never corrected from when the job was first created.

❌ Before — 11:30 AM local time
"expr": "30 11 * * 0-4"
"tz": "Your/Timezone"
✓ After — 2:30 PM local time
"expr": "30 14 * * 0-4"
"tz": "Your/Timezone"

Problem 3 — Timezone disappeared after CLI edit

When we first fixed the cron expression via CLI without specifying --tz, OpenClaw dropped the timezone field entirely — and the UI defaulted it to an incorrect timezone. 30 14 * * 0-4 in LA time = wrong time entirely.

๐Ÿ’ก
Always specify --tz explicitly

When editing a cron job via CLI, always include --tz "Your/Timezone" even if you're not changing the timezone. OpenClaw may drop the field otherwise and the UI will silently default to a different timezone.

The Fix — One CLI Command

terminal
# Fix expression + timezone + wakeMode in one shot
openclaw cron edit <job-id> \
  --cron "30 14 * * 0-4" \
  --tz "Your/Timezone" \
  --wake next-heartbeat

# Verify in the UI — check timezone shows your local timezone, not a default
Final cron state

30 14 * * 0-4 @ Your/Timezone · wakeMode: next-heartbeat · timeout: 300s · lightContext: true · next run: Thursday 2:30 PM

๐Ÿท️
Side effect: rename everything atomically

Renaming a skill directory or memory file means updating every reference — scripts, SKILL.md, MEMORY.md, and the cron payload. Missing one causes a silent failure at the next scheduled run. Always follow a rename with grep -r "old-name" /config/clawd/ to confirm zero remaining references.

Stretching the Gemini Free Tier

Running OpenClaw on the Gemini free tier until the setup proves its value. Heavy testing sessions burn through quota fast — here's what actually consumes tokens and how to minimize it.

What Costs Tokens

ActionLLM CallsApprox Tokens
Telegram trigger (pf, top signals)2~3,500
Cron job run (full context)2~4,000
Cron job run (lightContext)2~1,800
SSH script test00
Repeated testing via Telegram2 per testadds up fast

lightContext Flag

Added to both crons — reduces the bootstrap context loaded per isolated session. Safe for cron jobs since they have a specific task in the payload and don't need full memory/skill context.

openclaw cron edit <job-id> --light-context
# Adds "lightContext": true to the payload
# Reduces tokens per cron run by ~55%

Testing Strategy

Rule 01

Test scripts directly via SSH — python3 signals.py --top uses zero API calls. Only use Telegram to verify the final trigger works end-to-end, not for iterative testing.

Rule 02

Free tier resets hourly. If you hit the limit, wait 10-15 minutes. Daily limit resets at midnight Pacific time. Don't switch providers just because of a temporary limit.

Rule 03

Keep MEMORY.md and SKILL.md concise. Every line loaded into context costs tokens on every turn. Remove redundant rules and consolidate where possible.

Rule 04

The two daily crons (2:30 PM portfolio + midnight backup) consume roughly 11,600 tokens/day on lightContext. Well within Gemini free tier limits for regular usage.

5 Lessons from a Debugging Session

None of these are obvious until you've hit them. Each one cost at least 20 minutes to diagnose.
Lesson 01 — Use the CLI, not the JSON file

Editing /config/.openclaw/cron/jobs.json directly gets overwritten when OpenClaw saves state. Always use openclaw cron edit <id> --flag value. Run openclaw cron edit --help first — the available flags are not what you'd guess.

Lesson 02 — Check the UI screenshot after every cron edit

The OpenClaw web UI is the ground truth for what the scheduler actually sees. After any CLI edit, open the cron settings and verify every field — especially timezone. A missing --tz flag silently defaults to an incorrect timezone in the UI.

Lesson 03 — Rename files AND update every reference atomically

When you rename a file, grep for every reference: scripts, SKILL.md, MEMORY.md, cron payloads, and the actual filesystem. Missing one means a silent failure at the worst possible time — during a scheduled cron run.

Lesson 04 — Store SSH keys in /config/.ssh not ~/.ssh

In the OpenClaw HAOS add-on container, ~ is /config anyway, but explicitly using /config/.ssh makes the intent clear: this is persistent storage. Container restarts don't wipe /config. They do wipe everything else.

Lesson 05 — Scripts belong in git, not just on the Pi

A single git push from your PC + "pull scripts" on Telegram is faster and safer than SSH + nano + copy-paste. Version history means every mistake is recoverable. The midnight auto-push means you always have yesterday's working version.

Current Cron Summary
JobSchedulewakeModelightContext
Portfolio Update 2:30 PM30 14 * * 0-4 Your/Timezonenext-heartbeat
GitHub Backup Midnight0 21 * * * Your/Timezonenext-heartbeat
๐Ÿ”ญ
What's next

Thursday 2:30 PM is the first real test of the fixed cron — portfolio.py output via lightContext Gemini. If the format is clean, the system is stable. Part 6 will cover signal refinement and potentially a direct Telegram webhook to bypass the LLM entirely for price checks.

Comments