nightly-backup — Hermes Backup Automation
Thin wrapper around hermes backup + encryption + optional remote upload + retention.
Procedure
Snapshot. Run:
STAMP=$(date +%Y%m%d-%H%M%S) hermes backup -o /tmp/hermes-backup-$STAMP.ziphermes backupwrites a zip of the Hermes home (config, skills, sessions, memory, cron jobs, logs) per Part 16.-q/--quicktakes a fast snapshot of the critical state files instead.Encrypt. Non-interactively — this runs from cron, so nothing may prompt.
Route A — age with a keyfile (preferred):
# One-time setup (NOT in the nightly run): generate the identity, store a # copy somewhere that is not this host. age-keygen -o ~/.age-backup-key && chmod 600 ~/.age-backup-key # Nightly: encrypt to the key's recipient (public half) — no prompt, and # the nightly path never needs the private key at all. age -r "$(age-keygen -y ~/.age-backup-key)" \ -o /tmp/hermes-backup-$STAMP.tar.age /tmp/hermes-backup-$STAMP.tar(
age -pis interactive passphrase mode — it cannot run from cron.)Route B — gpg symmetric with a passphrase:
BACKUP_PASSPHRASE="${BACKUP_PASSPHRASE:-}" # exported from ~/.hermes/.env via the cron/systemd EnvironmentFile gpg --batch --yes --symmetric --cipher-algo AES256 \ --passphrase "$BACKUP_PASSPHRASE" \ /tmp/hermes-backup-$STAMP.zip(There is no
hermes secrets getfor plain env vars —hermes secretsonly manages Bitwarden/1Password vault sources. Read the passphrase from the environment instead, or pull it from a vault at runtime.)Then either way:
shred -u /tmp/hermes-backup-$STAMP.zipUpload. Based on
remote:parameter:s3://…→aws s3 cp <file> s3://bucket/prefix/b2://…→rclone copy <file> b2:bucket/prefix/ssh://…→rsync -av <file> user@host:/path/local→ move to~/.hermes/backups/
Prune. Delete anything older than
retain_days:s3: use S3 lifecycle policy if possible; otherwiseaws s3 ls+ age filterb2:rclone delete --min-age ${retain_days}d b2:bucket/prefix/ssh:ssh host "find /path -mtime +${retain_days} -delete"local:find ~/.hermes/backups -mtime +${retain_days} -delete
Verify. Download a random recent backup and test-decrypt:
# Route A (age keyfile): age -d -i ~/.age-backup-key backup.zip.age > /tmp/verify.zip # Route B (gpg passphrase): # gpg --batch --passphrase "$BACKUP_PASSPHRASE" -d backup.zip.gpg > /tmp/verify.zip unzip -t /tmp/verify.zip | tail -3 && shred -u /tmp/verify.zipFail loud if the verification fails — a backup you can't restore is not a backup.
Report. Send a line to your configured
notify:channel:✔ hermes backup 2026-04-17 — 284 MB, uploaded to s3://backups/hermes/, pruned 3 oldOn failure, send 🔴 with the specific error and skip pruning (keep old backups until the new one succeeds).
Cron wiring
Jobs live in ~/.hermes/cron/jobs.json and are managed via hermes cron create — the old cron.yaml list format was removed upstream:
hermes cron create "0 3 * * *" \
"Run the nightly-backup skill with remote=s3://my-backups/hermes/ retain_days=30" \
--skill nightly-backup --name nightly-backup --deliver telegram
Security notes
hermes backuparchives the Hermes home — as of v0.20 the--quicksnapshot explicitly includes.envandauth.json. Do not assume the archive excludes secrets: inspect what ships (unzip -l backup.zip), and treat the encryption step above as the real protection at rest.- The decryption secret must live outside this host's
.env— Route A: keep an offline copy of~/.age-backup-key(that file is the only way back into your archives); Route B: keepBACKUP_PASSPHRASEin a separate secret store. Otherwise a stolen Hermes host gets both the backups and the key to them. - Rotate the backup key/passphrase yearly with
skills/security/rotate-secrets, then re-encrypt (or at least re-verify) the archives you still need.