Skip to content

cactusaudio/cactus-cc-sync

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cactus-cc-sync

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.

Why

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 --update alone is unsafe (silently loses changes on mtime conflicts)
  • iCloud Drive doesn't reliably sync deep dot-directories
  • Symlinking ~/.claude to 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.

Prerequisites

  • macOS only (uses macOS-specific tools: security for keychain, osascript-free, BSD openrsync)
  • 2+ machines reachable to each other via SSH (mDNS <hostname>.local or static IPs)
  • SSH key auth between machines (recommended) or willingness to use password fallback

Install

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 init

The wizard will:

  1. Detect this machine (whoami@hostname-s)
  2. Ask how many machines to sync (2–10)
  3. Ask username + hostname for each
  4. Detect if your machines have different usernames; offer optional CANONICAL_USER for cross-user namespace mode
  5. Probe SSH key auth and recommend ssh-copy-id for any peers needing setup

After running on each machine:

# Easy way: ask Claude Code in any session:
/sync

# Direct:
bash ~/.claude/skills/sync/sync.sh

Usage modes

sync.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)

Recommended workflow

Sync is user-triggered, not background-watched. Recommended cadence:

  • Finishing work on a machine/sync (push changes out)
  • Switching machines/sync first 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.

What gets synced

Under ~/.claude/:

  • skills/ — installed skills
  • commands/ — slash commands
  • settings.json — Claude Code config
  • projects/-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)

Auth options

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 machine

For 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 keychain

This 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).

Cross-user namespace mode

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/memory so 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 whoami while content stays unified.

If all your machines share the same username, leave CANONICAL_USER unset.

Safety mechanisms

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)

Audit log

# 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

Files

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

Adding / removing a machine

Edit nodes.conf:

NODES="alice@laptop alice@desktop bob@workstation new@machine"

Then on each machine:

  1. ssh-copy-id new@machine.local (set up key auth)
  2. (Optional) bash setup.sh keychain to add password fallback
  3. bash setup.sh to verify

Known limitations

  1. mtime conflict on near-simultaneous writes: protected files gate via 3-way; non-protected take newer mtime.
  2. No multi-writer locking: simultaneous sync from multiple machines is a race. Serial user-triggered only.
  3. macOS Keychain ssh-batch limitation: security add-generic-password writes need a GUI Terminal session.
  4. macOS openrsync --backup-dir space bug: backup paths must not contain spaces. brew install rsync for GNU rsync removes the limit.

Architecture rationale (the audit lineage)

This skill went through several iterations after audits caught real bugs:

  1. Initial naive rsync --update once silently overwrote a MEMORY.md (60 → 3 lines) due to mtime ordering. Added local pre-sync backup + .md shrink-rollback.
  2. 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.
  3. Push had no remote backup → if push corrupted a peer's file, no recovery. Added rsync --backup --backup-dir for push targets, plus post-guard rollback that uses those backups.
  4. rsync errors silently dropped from summary. Added explicit ERR:<rc> propagation + audit event.
  5. Audit was single-line text. Replaced with JSONL outside ~/.claude/ so the audit log isn't subject to its own sync.
  6. Added --inspect-only and --dry-run (true zero-mutation, all phases gated by READONLY flag).
  7. 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.

License

MIT — see LICENSE.

Contributing

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.

About

Conservative multi-machine sync for Claude Code state (~/.claude/) across N macOS machines. SSH key auth, path-rewrite per user, 3-way conflict guard, JSONL audit log.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages