Conservative multi-machine sync for Claude Code state.
Syncs your ~/.claude/ (skills, slash commands, settings, project memory) across N macOS machines so you can move between laptop/desktop/workstation and pick up where you left off.
Not a code-sync tool. Use git for code. This skill is for the metadata Claude Code accumulates: skills you've installed, slash commands, project-scoped memory, and per-cwd notes.
Claude Code stores per-project memory and learned context under ~/.claude/projects/-Users-<you>-<cwd>/memory/. If you have multiple machines (laptop + desktop + workstation), each machine accumulates separate memory. This skill keeps them in sync.
Built specifically because:
rsync --updatealone is unsafe (silently loses changes on mtime conflicts)- iCloud Drive doesn't reliably sync deep dot-directories
- Symlinking
~/.claudeto Dropbox/iCloud breaks Claude Code internals git-based approaches require explicit commit cadence; user-triggered sync is closer to natural workflow
This skill is conservative: pre-sync local backups, remote backups (overwritten files preserved on peer), 3-way conflict guard on important files, post-sync rollback, JSONL audit log.
- macOS only (uses macOS-specific tools:
securityfor keychain,osascript-free, BSDopenrsync) - 2+ machines reachable to each other via SSH (mDNS
<hostname>.localor static IPs) - SSH key auth between machines (recommended) or willingness to use password fallback
On each machine:
# Drop into your Claude Code skills directory:
git clone https://github.com/<your-fork>/cactus-cc-sync.git ~/.claude/skills/sync
# First-time setup (interactive wizard):
bash ~/.claude/skills/sync/setup.sh initThe wizard will:
- Detect this machine (
whoami@hostname-s) - Ask how many machines to sync (2–10)
- Ask username + hostname for each
- Detect if your machines have different usernames; offer optional
CANONICAL_USERfor cross-user namespace mode - Probe SSH key auth and recommend
ssh-copy-idfor any peers needing setup
After running on each machine:
# Easy way: ask Claude Code in any session:
/sync
# Direct:
bash ~/.claude/skills/sync/sync.shsync.sh # normal sync
sync.sh --dry-run # preview only, zero mutation
sync.sh --inspect-only # read-only peer probe + conflict scan, then exit
sync.sh --force-protected # proceed past 3-way conflicts (mtime newer wins; may lose changes)Sync is user-triggered, not background-watched. Recommended cadence:
- Finishing work on a machine →
/sync(push changes out) - Switching machines →
/syncfirst thing on the new machine (pull peers' changes in) - Don't run sync on multiple machines simultaneously (race condition; treat as serial)
This keeps all machines ≤ 1 sync-cycle out of date.
Under ~/.claude/:
skills/— installed skillscommands/— slash commandssettings.json— Claude Code configprojects/-Users-<self>*/memory/— Claude's per-cwd memory directories (path-rewritten on transfer to peer's namespace)
What does not sync:
cache/,backups/,file-history/,session-env/,paste-cache/,downloads/,plugins/cache/history.jsonl,.DS_Store,__pycache__/.secrets,.secrets.migrated_*(legacy plaintext password files; explicit excludes)
SSH key auth (recommended) — passwordless, no secrets on disk:
[ -f ~/.ssh/id_ed25519 ] || ssh-keygen -t ed25519 -N '' -f ~/.ssh/id_ed25519
ssh-copy-id <user>@<peer>.local # one-time per peer per machineFor full mesh (each of N machines can SSH to others without password): ssh-copy-id from each machine to every other.
Password fallback (optional, opt-in) — passwords stored in macOS Keychain:
bash ~/.claude/skills/sync/setup.sh keychainThis is interactive: prompts for each peer's password (hidden input), writes to security add-generic-password under service claude-sync. Must run in a physical GUI Terminal session (ssh-batch sessions can't write a locked login keychain).
sshpass is auto-installed via Homebrew if not present (only when needed).
If your machines have different usernames but you want them to share one memory namespace, set CANONICAL_USER in nodes.conf. The wizard offers this when it detects mixed usernames.
Example: alice has two laptops (logged in as alice) and a workstation (logged in as bob). She sets CANONICAL_USER="alice". Now:
- On the bob-named workstation, sync.sh symlinks
~/.claude/projects/-Users-bob/memory → -Users-alice/memoryso local Claude sessions auto-load alice's main memory. - Sync transport path-rewrites memory dirs so each machine's physical paths stay matched to its own
whoamiwhile content stays unified.
If all your machines share the same username, leave CANONICAL_USER unset.
| Layer | What |
|---|---|
| Pre-sync local backup | ~/Documents/Claude/claude_code_backup/sync_<RUN_ID>/ (top 100 retained) |
| Pull-overwrite local backup | rsync --backup-dir for files local had that pull overwrote |
| Push-overwrite remote backup | rsync --backup-dir on peer for files peer had that push overwrote (top 100 retained) |
| 3-way conflict guard | Protected files (MEMORY.md, feedback_*.md, project_*.md, reference_*.md, user_*.md, SKILL.md, commands/*.md) compared against peer pre-sync state — block unless --force-protected if hashes differ AND lines diff > 30% |
| Post-sync rollback | After push, re-fetch peer state; if any protected file shrunk > 30%, auto-restore from remote backup-dir |
Local .md shrink rollback |
After all sync, verify any local .md shrunk > 30% vs pre-sync backup; auto-restore |
| JSONL audit log | ~/.claude-sync-audit/sync.jsonl (outside ~/.claude/, not synced) |
# All events in one run:
jq -c 'select(.run_id=="<RUN_ID>")' ~/.claude-sync-audit/sync.jsonl
# All conflicts ever:
jq -c 'select(.event=="conflict")' ~/.claude-sync-audit/sync.jsonl
# Runs that triggered rollbacks:
jq -c 'select(.event=="run_complete" and .status=="rolled_back")' ~/.claude-sync-audit/sync.jsonl| File | Purpose |
|---|---|
SKILL.md |
Skill manifest (loaded by Claude Code) |
sync.sh |
Main sync script |
setup.sh |
Setup wizard + verify + keychain helper |
nodes.conf |
User-specific config (gitignored; created by wizard) |
nodes.conf.example |
Template |
README.md |
This file |
Edit nodes.conf:
NODES="alice@laptop alice@desktop bob@workstation new@machine"
Then on each machine:
ssh-copy-id new@machine.local(set up key auth)- (Optional)
bash setup.sh keychainto add password fallback bash setup.shto verify
- mtime conflict on near-simultaneous writes: protected files gate via 3-way; non-protected take newer mtime.
- No multi-writer locking: simultaneous sync from multiple machines is a race. Serial user-triggered only.
- macOS Keychain ssh-batch limitation:
security add-generic-passwordwrites need a GUI Terminal session. - macOS openrsync
--backup-dirspace bug: backup paths must not contain spaces.brew install rsyncfor GNU rsync removes the limit.
This skill went through several iterations after audits caught real bugs:
- Initial naive
rsync --updateonce silently overwrote aMEMORY.md(60 → 3 lines) due to mtime ordering. Added local pre-sync backup +.mdshrink-rollback. - Conflict detection initially compared local-vs-local-backup, which always equal — so the guard was a no-op. Fixed: now compares vs peer pre-sync state via ssh-batch fetch.
- Push had no remote backup → if push corrupted a peer's file, no recovery. Added
rsync --backup --backup-dirfor push targets, plus post-guard rollback that uses those backups. - rsync errors silently dropped from summary. Added explicit
ERR:<rc>propagation + audit event. - Audit was single-line text. Replaced with JSONL outside
~/.claude/so the audit log isn't subject to its own sync. - Added
--inspect-onlyand--dry-run(true zero-mutation, all phases gated byREADONLYflag). - Peer-only-dir discovery: ssh-enumerate each peer's user-namespace memory dirs, union into SYNC_PATHS, so a brand-new cwd dir on one machine gets pulled by all others.
MIT — see LICENSE.
Issues and PRs welcome. Please don't include real machine hostnames or usernames in examples; use alice@laptop / bob@workstation / etc.
If you find a corner case not covered (especially around cross-user namespace), please file an issue with the failing scenario.