Skip to content

Cross-device Claude session sync (manual push/pull merge via GCS) #35

Description

@tim-codes

Goal

When switching workstations, gather all Claude Code sessions/history/transcripts onto the current machine. Manual (or agent-invoked) trigger only — no background auto-sync (OneDrive-style) that could conflict with live sessions:

  • on-finish-workpush: merge this machine's state up to shared storage
  • on-pickup-workpull: merge shared state down before starting

What syncs (per context: ~/.claude-personal, ~/.claude-exxo)

Same durable/runtime split scripts/claude-contexts --adopt-default and the exxo-personal overlay already draw:

  • projects/ — transcripts + auto-memory (the payload that matters)
  • history.jsonl — prompt history (needs line-union merge, not file copy)
  • plans/, tasks/, todos/, file-history/, paste-cache/

Excluded: .claude.json (login/trust is per-machine), settings.json/CLAUDE.md/skills (claude-sync owns these from the repo), plugins (reinstallable snapshots), all runtime state (daemon, caches, session-env, shell-snapshots, sessions/). The ~/.claude-exxo-personal overlay needs nothing of its own — its durable state is symlinks into ~/.claude-exxo.

Proposed design: GCS bucket + advisory lock

One bucket (personal GCP project), one prefix per context: gs://<bucket>/claude-sync/<context>/....

Locking: GCS has no native lock API, but atomic lock-object creation via the if-generation-match=0 precondition is the standard advisory-lock pattern:

  • acquire: gcloud storage cp lock.json gs://…/lock --if-generation-match=0 (fails atomically if held)
  • lock body: {host, pid, timestamp}; stale after N minutes → steal with a warning
  • release: delete on completion

Merge semantics:

  • projects/ and other dirs: per-file newest-wins (gcloud storage rsync, no --delete-unmatched-destination-objects). Transcripts are uuid-named and effectively single-writer (a session runs on one machine), so real conflicts shouldn't occur; a same-file-both-newer clash is logged, never silently resolved.
  • history.jsonl: download remote, line-union with local (dedupe on full line or (timestamp, sessionId)), upload merged, keep merged locally.
  • Deletion does NOT propagate (rsync without delete) — cleanup/retention is a separate explicit operation.

Trigger surface: scripts/claude-session-sync <push|pull> [--context personal|exxo|all] in dotfiles, plus a small skill so any session can be told "I'm done on this machine" / "pick up from my other machine". Wrappers stay thin; explicit targets per the no-ambient-state rule; creds via the usual 1Password/esc reference flow — never a literal key.

Alternative considered: homelab hub (zima/ragnar) over rsync+ssh

No cloud dependency, data stays on-prem, ssh already trusted. Downsides: unavailable off-LAN/VPN when travelling with only laptops, and no cheap atomic lock primitive (flock over ssh works but is another moving part vs if-generation-match=0). GCS preferred; noting for the record.

Open questions

  • Exxo transcripts in a personal GCP bucket — acceptable data governance, or does exxo context need an Exxo-owned bucket/prefix with its own credentials?
  • Encryption: default at-rest is likely fine for personal; consider client-side (age) for the exxo prefix if governance demands.
  • Desktop app session records (~/Library/Application Support/Claude-*/claude-code-sessions/) are per-machine and reference cwd paths — out of scope initially; CLI --resume over synced projects/ is the target UX. Machines must share identical repo paths (~/dev/...) for slugs to line up — true today; note as a constraint.
  • Retention/pruning of the bucket copy (transcripts grow unbounded).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions