# Hermes Migration

> Restore Hermes from a backup repo or migrate machines.

- Skill: `ntizar/hermes-migration` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add ntizar/hermes-migration`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ntizar/hermes-migration/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- License: MIT
- Author: ntizar (https://skillmd.com/u/ntizar)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ntizar/hermes-migration

---


## When to Use

- The user asks to restore/reinstall their Hermes setup from a backup repo (e.g. their personal "Mastermind"/"Brain" repo on GitHub).
- A new machine or fresh Hermes install needs a previous agent setup (skills, memories, SOUL, scripts, cron) carried over.
- Porting Hermes scripts/wrappers between platforms (Linux VM → Windows desktop).

# Hermes Migration / Restore from Backup

How to bring a personal Hermes setup (skills, memories, SOUL, scripts, cron) from a backup repo or an old machine into a live Hermes install — especially cross-platform (Linux MicroVM → Windows desktop).

## Step 0 — Inventory before copying

Clone/list the backup and map what exists. Typical backup repo layout and its destination in a live install (`$LOCALAPPDATA/hermes` on Windows, `~/.hermes` elsewhere):

| Backup item | Destination | Notes |
|---|---|---|
| `skills/` or `hermes-home/skills/` | `<hermes-home>/skills/` | Multiple copies may exist in the backup — identify the LIVE one (has `.curator_state`, recent dates) |
| `memories/` (MEMORY.md, USER.md, INDEX.yaml) | `<hermes-home>/memories/` | Format: entries separated by `§` on their own line |
| `SOUL.md`, `user.md` | `<hermes-home>/` or as `SOUL.repo.md` reference copy | Don't overwrite an existing SOUL without asking |
| `scripts/` | `<hermes-home>/scripts/` (or a `restored/` subdir) | Often Linux paths inside — needs porting |
| `config.yaml` | Reference only | Don't overwrite live config; the model/provider usually changed |
| `notes/`, `data/` | Repo / workspace | Keep as archive or working data |

Ask the user two things before touching anything: (1) skills — add-only (skip ones that already exist) or overwrite; (2) scope — skills+memories only, or also scripts/SOUL.

## Step 1 — Copy skills (add-only pattern)

```bash
added=0; skipped=0
for src in "$R/hermes-home/skills" "$R/skills"; do
  for d in "$src"/*/; do
    n=$(basename "$d")
    [ -e "$H/skills/$n" ] && { skipped=$((skipped+1)); continue; }
    cp -r "$d" "$H/skills/$n" && added=$((added+1))
  done
done
echo "added: $added, skipped (existed): $skipped"
```

If the backup has two skill trees that differ, diff each shared name; the tree with curator state files (`.curator_state`, `.usage.json`) is the live/newer one.

## Step 2 — Merge memories without loss

Memory files use `§`-separated entries. Union both sources, dedupe by normalized prefix:

```python
def union(f1, f2, out):
    parts = [p.strip() for p in (open(f1, encoding='utf-8-sig').read().split('§')
                                + open(f2, encoding='utf-8-sig').read().split('§'))]
    seen, uniq = set(), []
    for p in parts:
        key = p.replace('\r','').replace('\n',' ')[:80]
        if p and key not in seen: seen.add(key); uniq.append(p)
    open(out, 'w', encoding='utf-8', newline='\n').write('\n§\n'.join(uniq) + '\n')
```

Never pick one side wholesale — the old-machine copy often has entries the root copy lacks and vice versa.

## Step 3 — Port scripts and secrets handling (Linux → Windows)

- Wrappers that `source /path/.env` break. Preferred pattern: get the GitHub token from the CLI (`GITHUB_TOKEN=$(gh auth token)`) and resolve paths relative to the script (`SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"`). This also keeps secrets out of cron prompts.
- Replace absolute Linux paths (`/hermes-home/...`, `/root/workspace/...`) with repo-relative or env-var paths. Grep the whole backup for both patterns before declaring done.
- `python3` may be missing on Windows (`python` instead); scripts calling `python3` directly fail — route through the wrapper.
- Verify each ported wrapper runs: `bash script.sh --status` before wiring any cron to it.

## Step 4 — Recreate cron jobs

Old cron jobs do not migrate — only their prompts live in backup docs/skills. Recreate with `cronjob(create)` using self-contained prompts (fresh session, no chat context): absolute repo path, script invocation, decision criteria, report format, and rules ("never read .env directly"). See the cron-scanner pitfall below.

## Step 5 — Activate gateway + crons on the new machine

Cron jobs recreated via `cronjob(create)` are saved but will NOT fire until the gateway runs:

```bash
hermes gateway install   # Windows: UAC prompt for Scheduled Task; without elevation falls back to a Startup-folder .vbs (still works — auto-starts at login)
hermes gateway status    # verify "Gateway process running"
```

- After changing the global model with `hermes config set model.default ...`, enabled cron jobs with a stored model/provider snapshot **fail closed** on their next run (they do not silently pick up the new model). Pin each job: `hermes cron edit <job_id> --model <model> --provider <provider>`.
- Local-only cron delivery saves output to `%LOCALAPPDATA%\hermes\cron\output\<job_id>\<ts>.md` instead of messaging the user; live delivery (e.g. Telegram) needs a gateway-connected channel.

## Verification

- `find "$H/skills" -iname SKILL.md | wc -l` — count grew by the number added.
- `grep -ril <old-agent-name> "$H/skills" "$H/memories"` — 0 hits if the user renamed the agent.
- Wrapper smoke test (`--status`) returns registry/stats, not a path error.

## Pitfalls

- **Cron scanner blocks secret-reading prompts.** Cron prompts that contain patterns like `cat .env` are rejected by the security scanner (it scans the assembled prompt + skills). Put secret loading inside a wrapper script; the cron prompt only calls the wrapper. NEVER put the secret-reading pattern in the prompt or in a skill that crons load.
- **"Device or resource busy" when moving a folder.** A folder anchored as a session/project cwd can't be `mv`-ed. Workaround: `cp -a src dst`, then remove the original (with approval).
- **Destructive and non-destructive ops in ONE terminal call get blocked together.** Split: do all copies/moves first, then issue deletes as a separate call so the approval prompt maps to the actual action. A silent approval timeout means NOT consented — never retry the same command, ask the user instead.
- **Oversized inline shell payloads hit the hardline parser block.** Multi-loop one-liners with many substitutions can be rejected as unparseable; the terminal tool saves the command to `%LOCALAPPDATA%\hermes\cache\blocked-scripts\blocked-<id>.sh` — just run `bash <that path>` instead of re-typing.
- **CRLF noise on Windows repos.** `core.autocrlf=true` makes untouched files show as modified forever. Fix once with a `.gitattributes` containing `* -text`, then `git add -A && git commit`. (Only for personal repos where byte-exact content is fine.)
- **git-bash cwd quirk:** `git -C /c/...` fails with MSYS path conversion disabled; use `cd <dir> && git ...` inside the same call.
- **MSYS paths silently swallowed by native Python (verified data-loss bug).** A bash wrapper exporting `VAR=/c/Users/...` and calling native `python script.py` makes Python resolve `/c/Users/...` as `C:\c\Users\...` — file writes land in a phantom `C:\c\` tree and the real file never changes, with NO error. Symptom: script prints success but the target file's mtime/content is unchanged. Fix: export **Windows-native paths** (`C:/Users/...`) for anything consumed by native Python; `cygpath -w` if needed. Symptom-check: `find /c -maxdepth 4 -name <file>` after an unexplained no-op write.
- **Model/provider config changes vs stored cron snapshots:** see Step 5 — pin cron jobs after switching models or they fail closed.
- **NaN API (api.nan.builders/v1) rejects urllib requests without a custom User-Agent** with HTTP 403, while curl with the same payload succeeds. Always set `{"User-Agent": "..."}` on `urllib.request` calls to it.
- **pip installs into the wrong interpreter.** On this setup the default `python`/`pip` on PATH point to the Hermes venv (`%LOCALAPPDATA%\hermes\hermes-agent\venv`) which has no pip; system Python is `%LOCALAPPDATA%\Programs\Python\Python312\python.exe`. Install with the full path: `"<system python>" -m pip install <pkg>`, and invoke scripts with the full path too. Check with `python -c "import sys; print(sys.executable)"` before assuming an install failed.

## User preferences to carry into any migration (2026-08 session)

- Language: Spanish for reports, docs, and cron prompts.
- Add-only skill restore; never silently overwrite the live install's existing skills.
- Personal agent name is Mastermind (owner: David Antizar / Ntizar). Do not use other historical agent names in any restored content.
- Repo conventions for the personal MasterMind repo: never delete from it (create/modify only), everything in Spanish, and never hard-code skill counts in docs/crons — they grow with every learning cycle; write "el número no es fijo" and point to the indexer command instead.
- Skill counts / stats in any doc stay OPEN (no fixed numbers) by explicit user request.
- SOUL.md of the backup repo, once cleaned (old agent names removed), becomes the live agent's SOUL.md — the user wants identity carried over "a tope", not kept as a reference copy.

## References
- `references/backup-repo-restore.md` — receta end-to-end de un restore real (paths, merge script, secuencia de reorg, notas de recreación de crons). Movido desde el skill duplicado `hermes-setup-migration` al consolidar (2026-09-08).

