| name | skill-sync |
| description | Sync skills between machines over SSH and Tailscale. |
| version | 1.0.0 |
| author | alt-glitch (alt-glitch), Hermes Agent |
| license | MIT |
| platforms | ["linux","macos"] |
| metadata | {"hermes":{"tags":["Skills","Sync","SSH","Tailscale","Rsync","Multi-Machine","Cron"],"category":"devops","related_skills":[]}} |
Skill Sync
Sync ~/.hermes/skills/ between machines over SSH. Compares modification
times, pulls newer skills, discovers new ones, and can push the local tree to
a fresh box. Conflict rule is last-writer-wins by mtime; sync is additive
(nothing is deleted on either side). Works over any SSH route — Tailscale is
the recommended transport because it gives every machine a stable name that
works across networks.
When to Use
- User has Hermes on two or more machines and wants skills kept in step
- User got a new machine and wants their skill library on it ("push my skills to the new box")
- User improved a skill on one box and wants it everywhere
- User asks for automatic/scheduled skill syncing between machines
- A skill exists on another machine but not here
Prerequisites — walk the user through these, don't assume them
Each remote needs: a network route, a running sshd, key-based auth, and
rsync. Run the doctor first — it checks all four and prints the exact fix
for anything missing:
terminal(command="bash ~/.hermes/skills/skill-sync/scripts/doctor.sh <user@host>")
If the doctor reports failures, guide the user through them in this order:
- Network route (Tailscale, recommended). If the machines aren't on the
same LAN, have the user install Tailscale on both ends
(https://tailscale.com/download) and run
tailscale up on each, logged
into the same tailnet. Verify with tailscale status — every machine gets
a stable node name and a 100.x.y.z IP that work from anywhere. Any other
SSH route (LAN hostname, VPN, public IP) also works.
- sshd on the target. Fresh macOS refuses SSH by default — the user must
enable System Settings → General → Sharing → Remote Login, or run
sudo systemsetup -setremotelogin on. Linux: sudo apt install openssh-server && sudo systemctl enable --now ssh. Tailscale users can
skip sshd entirely with sudo tailscale set --ssh on the target.
- Key auth. No password prompts — the scripts run with
BatchMode=yes.
If the user has no keypair: ssh-keygen -t ed25519. Then authorize it:
ssh-copy-id <user@host> (one password entry, ever).
- The right login name. With Tailscale the HOST is the node name from
tailscale status, but the USER is that box's unix login — they usually
differ, and the tailnet account name is often NOT an authorized login.
Probe candidates before syncing:
ssh -o BatchMode=yes -o ConnectTimeout=8 <candidate>@<host> 'echo OK'
Permission denied (publickey) = wrong user or key not copied.
Connection refused = sshd not running (step 2).
Re-run the doctor after each fix until it prints "all checks passed".
How to Run
Invoke through the terminal tool. Scripts live in the skill's scripts/
directory — shown below as ~/.hermes/skills/skill-sync/scripts/; if the
skill was installed into a category, add that segment (e.g.
~/.hermes/skills/devops/skill-sync/scripts/). Remotes are
always explicit — positional args or SKILL_SYNC_REMOTES (comma-separated).
bash ~/.hermes/skills/skill-sync/scripts/sync.sh user@host
bash ~/.hermes/skills/skill-sync/scripts/sync.sh --push user@host
DRY_RUN=1 bash ~/.hermes/skills/skill-sync/scripts/sync.sh user@host
Quick Reference
| Command | What it does |
|---|
scripts/doctor.sh <user@host> | Verify Tailscale/SSH/rsync; prints fixes |
scripts/sync.sh <user@host> | Pull remote-newer + remote-only skills |
scripts/sync.sh --push <user@host> | Push local-newer + local-only skills |
DRY_RUN=1 scripts/sync.sh ... | Preview; read the [^]/[+] lines |
scripts/p2p_sync.py <user@host> | Per-skill triage picker (no transfers) |
Procedure
First-time pairing
bash scripts/doctor.sh <user@host> — fix anything it flags (see Prerequisites).
DRY_RUN=1 bash scripts/sync.sh <user@host> — show the user the plan. The change set
is the union of [^] (update) and [+] (new) lines.
bash scripts/sync.sh <user@host> (or --push for a new-machine bootstrap).
- Verify (see Verification).
Selective sync
When the user only wants some skills moved, run
python3 ~/.hermes/skills/skill-sync/scripts/p2p_sync.py <user@host> (scripts/p2p_sync.py).
It prints a numbered PULL/PUSH picker (new / updated / divergent per skill)
and transfers nothing. Present the picker, get the user's selection, then
rsync the chosen skill directories yourself:
rsync -azL -e "ssh -o BatchMode=yes" "user@host:.hermes/skills/<cat>/<skill>/" ~/.hermes/skills/<cat>/<skill>/
Scheduled sync (cron)
The cron runner passes no args, so bake the remotes into a wrapper:
mkdir -p ~/.hermes/scripts
printf '#!/usr/bin/env bash\nSKILL_SYNC_REMOTES="user@host" exec bash ~/.hermes/skills/skill-sync/scripts/sync.sh\n' > ~/.hermes/scripts/skill-sync-tick.sh
chmod +x ~/.hermes/scripts/skill-sync-tick.sh
hermes cron create "every 6h" --name skill-sync --script ~/.hermes/scripts/skill-sync-tick.sh --no-agent
Conflict Resolution
The unit of sync is the whole skill directory (SKILL.md + references/ +
scripts/ + templates/), transferred atomically via rsync. The rule is
last-writer-wins by SKILL.md mtime — skills are single-author documents,
and this matches how they evolve: one machine gets the fix, the others pick
it up.
Exception — the same skill forked on two machines. If BOTH sides added
unique content, last-writer-wins is LOSSY: the newer mtime overwrites
wholesale and drops the loser's additions. p2p_sync.py flags these as
divergent. Do NOT sync a divergent skill — do a manual union merge; full
procedure in references/merging-forked-skill-copies.md.
Pitfalls
- mtime lies about content. A copy can be newer AND missing files the
older copy has (fork trap). When in doubt, compare file inventories:
ssh <remote> 'find ~/.hermes/skills/<cat>/<skill> -type f' vs local.
DRY_RUN=1 plan = the [^] + [+] lines, not just the summary count.
- Never trust "Push complete" — verify on the remote.
sync.sh prints a
remote skill count after every push; if it doesn't move, the transfer
failed. Spot-check specific paths too (see Verification).
- Whole-tree fallback when per-skill sync misbehaves: one additive pass,
no
--delete, surfaces errors:
rsync -azL -e "ssh -o BatchMode=yes" ~/.hermes/skills/ user@host:.hermes/skills/
- Only one SSH direction may work. If push to a box is refused but that
box can SSH back here, flip it: have the other machine's agent PULL. Write
it a handoff note with the source
user@host and the exact sync.sh
command rather than fighting the dead direction.
- Sync is additive — it never deletes local skills missing on the remote.
Within an updated skill,
--delete prunes files the newer side removed.
- Skills under paths containing
.bak or .archive are always excluded,
both directions.
- macOS vs Linux
stat flags differ; the scripts handle both.
- Byte-comparing across OSes: macOS
wc -c left-pads output — tr -d ' '
before comparing.
Verification
ssh -o BatchMode=yes user@host 'find ~/.hermes/skills -name SKILL.md | wc -l'
ssh -o BatchMode=yes user@host 'find ~/.hermes/skills/<cat>/<skill> -type f | sed "s|.*/skills/||"'
A second DRY_RUN=1 sync.sh run should report zero [^]/[+] lines — the
trees have converged.