Sync Claude Code sessions between two machines over SSH — no cloud.
Project description
Start a Claude Code session on your Linux desktop, claude --resume it on
your Mac — or any pair of your machines. claude-hop syncs
~/.claude/projects/ over SSH with rsync, rewriting the absolute paths that
would otherwise make sessions invisible on the other side. No cloud, no
accounts: your machines, your keys, your sessions.
Quickstart (30 seconds)
uv tool install claude-hop # or: pipx install claude-hop
claude-hop init # asks for the SSH host, detects the rest
claude-hop push # then `claude --resume` on the other machine
No uv? One-liner that installs it first:
curl -sSL https://raw.githubusercontent.com/SeiduTakizawa/Claude-hop/main/install.sh | bash
Requirements: Python ≥ 3.11 on this machine; sshd and rsync on the other
one (key-based SSH auth recommended).
See it in action
Real commands against a sandbox — scripted with VHS, so it re-renders instead of re-recording.
The problem it solves
Claude Code stores sessions in ~/.claude/projects/, indexed by the
absolute path of the project with every non-alphanumeric character
replaced by a dash:
/home/alice/work/webshop → ~/.claude/projects/-home-alice-work-webshop/
The absolute paths also appear inside the session JSONL files — cwd
fields, file paths in tool calls. Copy the files verbatim from a Linux box
(/home/alice) to a Mac (/Users/alice) and claude --resume can never
find them: the directory name encodes the wrong path and the transcript
still references the wrong filesystem.
claude-hop rewrites both during sync — directory names and JSONL
contents — mapping your home here to your home there (auto-detected at
init), in whichever direction you sync. The rewrite is boundary-aware
(/home/al never matches inside /home/alice) and round-trip safe: push
then pull reproduces your files byte for byte.
Commands
| Command | What it does |
|---|---|
claude-hop init |
Interactive setup: SSH host, connection test, remote home auto-detection, rsync checks. With an existing config it offers to add another remote. |
claude-hop status [REMOTE] |
Table of local projects, session counts, and the remapped name each gets on the remote; flags ambiguous mappings. |
claude-hop diff [REMOTE] |
What would sync, in both directions, without writing anything. |
claude-hop push [REMOTE] |
Send local sessions to a remote machine. --all pushes to every remote. |
claude-hop pull [REMOTE] |
Fetch a remote's sessions and merge them in. --all pulls from every remote. |
claude-hop doctor [REMOTE] |
Environment checks: SSH, rsync versions, config, running Claude Code, stale staging dirs. Checks every remote by default. |
claude-hop remotes |
List configured remotes (--check probes them over SSH). |
claude-hop remotes add/remove/rename |
Manage remotes; add runs the same wizard as init. |
claude-hop remotes migrate |
Upgrade an old single-remote config to the multi-remote format (backs up the original first). |
claude-hop upgrade |
Upgrade claude-hop itself to the latest release (works for uv, pipx, and pip installs). |
push and pull accept --dry-run (-n), --yes (-y), --force, and --all.
Which remote runs? An explicit name wins (claude-hop push mac); with
exactly one remote configured the name can be omitted; with several and no
name, commands refuse and list your options — nothing is picked implicitly.
The first positional argument counts as a remote only when it exactly
matches a configured remote name; anything else is a project selector, and
a near-miss of a remote name is an error with a suggestion rather than a
silent no-op. If a directory in your cwd shares a remote's name, write
./name to mean the project.
Multiple machines are hub-and-spoke. This machine is the operator;
remotes only need sshd and rsync, and remotes never talk to each other.
To move sessions between two remotes, route through the hub —
claude-hop pull mac then claude-hop push thinkpad — the merge semantics
(newer file wins, never deletes) make that safe in any order.
Syncing a single project
push, pull, and diff take optional project selectors — by default they
sync everything:
claude-hop push . # just the project you're standing in
claude-hop pull ~/work/webshop # one project, by path
claude-hop push -- -home-alice-work-webshop # by encoded name, as shown in `status`
claude-hop diff . ~/work/blog # several at once
Encoded names start with a dash, so pass them after the standard --
separator — everything after -- is a selector, never an option.
Selectors are asymmetric on purpose: push matches them against your
local projects, while pull translates them through the mapping and
matches against the remote's projects — so pulling a project that doesn't
exist locally yet works.
When a selection is given, only those projects sync and the optional [sync]
extras (history/agents/skills) are skipped.
Safety model
- Merge, never clobber. rsync runs with
-u(newer file wins) and never with--delete— a sync can only add or update sessions, never remove. - Staging, not in-place. Remapping happens in a temp dir;
~/.claude/projects/is only ever touched by the merge itself. - Refuses to run alongside Claude Code. Sessions are flushed to disk on
exit, so syncing mid-session would miss your latest work. Override with
--forceif you know what you're doing. - First-pull backup. The first pull that would merge into existing local
sessions offers a tarball backup (
~/.claude/backups/) before touching anything.
Configuration
~/.config/claude-hop/config.toml (written by init, $XDG_CONFIG_HOME
honoured). Each machine you sync with is a named [remotes.<name>] table:
[remotes.mac]
host = "mac.local" # SSH alias or hostname; "" = a local directory
home = "/Users/alice" # auto-detected at init
[remotes.thinkpad]
host = "192.168.1.50"
home = "/home/al"
# per-remote mappings: merged over the global [mappings], per-remote wins
[remotes.mac.mappings]
"/home/alice/work/webshop" = "/Users/alice/projects/webshop"
[sync] # applies to all remotes
include_history = false # also sync ~/.claude/history.jsonl (remapped)
include_agents = false # also sync ~/.claude/agents/ (verbatim)
include_skills = false # also sync ~/.claude/skills/ (verbatim)
[mappings]
# Global: projects at *different relative paths* on the other machines.
# Specific mappings win over the generic home remap.
"/home/alice/work/blog" = "/srv/blog"
Configs from claude-hop ≤ 0.2.0 (a single [remote] table) keep working
and are read as a remote named default; run claude-hop remotes migrate
to upgrade the file in place (the original is backed up first).
claude-hop status shows exactly how every project will map; if one looks
wrong, add it under [mappings] (or the remote's own mappings table).
WSL / projects outside $HOME
The generic home remap only helps paths that contain your home. Projects
elsewhere — the classic case is WSL work under /mnt/c/... — need a
mapping prefix, or their sessions would land on the remote with paths
claude --resume can't find:
[remotes.mac.mappings]
"/mnt/c/Users/tasos" = "/Users/tasos/win"
You don't have to work this out yourself: status flags affected
projects, push/pull refuse them with a ready-to-paste snippet
(computed from the projects' real paths), and init/remotes add offer
to add the prefix mapping during setup. --force pushes them unchanged
if you really want that.
Supported platforms
Any pair of Unix-shaped machines, in either direction: Linux ↔ macOS,
Linux ↔ Linux, macOS ↔ macOS. The remap isn't tied to /home vs
/Users — it maps your home here to your home there, whatever they are
(identical homes on both sides work too; the remap just becomes a no-op and
you keep the merge/safety machinery).
Windows is supported via WSL: inside WSL you have a real Linux home,
rsync, and sshd, so claude-hop works there like on any Linux box — it
syncs the Claude Code you run inside WSL. Native (non-WSL) Windows is not
supported: Claude Code encodes C:\ project paths differently and there's
no native rsync. If you need it, open an issue.
Limitations worth knowing
- Sessions whose conversation text literally mentions the other machine's
home path (e.g. a transcript discussing
/Users/alice/...while on the Linux box) can't be rewritten losslessly — path rewriting is text-level by design, so such mentions get remapped too. - Session files that aren't valid UTF-8 are copied verbatim without remapping rather than risk corrupting them.
- On macOS 15+, the bundled
rsyncisopenrsync; it works, but if you hit option errors,brew install rsync.
Troubleshooting
- An update didn't come back after working on the other machine.
Merging is newer-file-wins, so clocks are load-bearing:
claude-hop doctorwarns when a remote's clock is skewed by more than 30 seconds. Runpush/pullwith--verboseto see any files a merge skipped because the destination copy was newer. - The resumed conversation shows up as a second session.
claude --resumecontinues a synced conversation into a new session file, so after syncing back you'll see both the original and the continuation in the picker. That's Claude Code's session model, not a sync bug — both files sync fine. - "host reachable, but key-based auth isn't set up". Probes never use
passwords (they'd hang invisibly), so password-only hosts show this
message. Run the suggested
ssh-copy-id user@hostonce, or point the config'shostat an~/.ssh/configalias that carries your key.
Development
uv sync # install with dev dependencies
uv run pytest # unit + integration tests (integration needs rsync)
uv run ruff check .
make e2e # full two-container SSH round trip (needs Docker)
The e2e rig (e2e/) spins up two containers — one with a Linux-style home,
one macOS-style — wires SSH key auth between them, and drives the real CLI
through push, modify-on-the-other-side, and pull.
Regenerating the demo
The README GIF is scripted, not hand-recorded: demo/setup.sh builds a
self-contained sandbox (fake home, two local-path remotes) and
demo/demo.tape replays real commands against it with
VHS. When CLI output changes, run
make demo (needs vhs, ttyd, ffmpeg) and commit the refreshed
assets/claude-hop.gif.
License
MIT — see LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file claude_hop-0.4.0.tar.gz.
File metadata
- Download URL: claude_hop-0.4.0.tar.gz
- Upload date:
- Size: 1.6 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.27 {"installer":{"name":"uv","version":"0.11.27","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
80fd99a05048abb130af0aba94982909ede2964dcf3d0eb812506c9b58ee5e14
|
|
| MD5 |
5b5c6367ab6e18b000308055987eddab
|
|
| BLAKE2b-256 |
b7eb3e938b238089a8eeb8d7374d7736d33a2be5fb25f371d49d4134861580ef
|
File details
Details for the file claude_hop-0.4.0-py3-none-any.whl.
File metadata
- Download URL: claude_hop-0.4.0-py3-none-any.whl
- Upload date:
- Size: 37.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.27 {"installer":{"name":"uv","version":"0.11.27","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d62c92ea5786d8cc0108dfc3f30290adbc4507ffc38752020a633f5630ece5dc
|
|
| MD5 |
1cc885d0905c7f8a8cdaf23b2cbef316
|
|
| BLAKE2b-256 |
0b7966f0c5344e5c24f8a2965665eaffdde74ff11f67138e0f6448f029f2501d
|