Run Claude Code locally with the UI on your machine, but execute all commands on a remote server.
Why? Claude Code can be CPU-intensive (TypeScript compilation, tests, file operations). This setup lets you:
- Keep your local machine fast and responsive
- Use a powerful remote server (EC2, etc.) for heavy lifting
- Maintain low-latency typing since the Claude UI runs locally
┌─────────────────────────────────────────────────────────────────────┐
│ Your Mac (Local) │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ Claude Code │───▶│ remote-shell │───▶│ SSH ControlMaster │ │
│ │ (UI/TUI) │ │ wrapper │ │ (persistent conn) │ │
│ └──────────────┘ └──────────────┘ └─────────┬──────────┘ │
│ │ │ │
│ ▼ │ │
│ ┌──────────────┐ ┌──────────────┐ │ │
│ │ Any local │◀──▶│ Mutagen │◀─────────────┼────────┐ │
│ │ directory │ │ (bidirectional sync) │ │ │
│ └──────────────┘ └──────────────┘ │ │ │
└────────────────────────────────────────────────────┼────────┼──────┘
│ │
▼ ▼
┌────────────────────────────────────────────────────────────────────┐
│ Remote Server (EC2) │
│ ┌──────────────┐ ┌──────────────────────────────────────┐ │
│ │ SSH │ │ REMOTE_MIRROR_ROOT/<abs-path>/ │ │
│ │ Server │───▶│ (your actual files & execution) │ │
│ └──────────────┘ └──────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────┘
-
Shell Interception: Claude Code uses
$SHELLto execute commands. We provide a custom shell wrapper that:- Intercepts all commands Claude tries to run
- Forwards them via SSH to the remote server
- Maps paths between local and remote automatically
- Returns output and exit codes transparently
-
Bidirectional File Sync: Mutagen keeps files in sync between local and remote:
- Files are real local files - instant access for Claude's Read/Write/Edit/Glob/Grep tools
- Works with any local editor (VS Code, Cursor, etc.)
- Before each remote command, sync is flushed to ensure consistency
- Smart ignore patterns for
node_modules,.venv,.cache, etc.
-
SSH Multiplexing: Uses SSH ControlMaster for persistent connections, avoiding SSH handshake overhead on every command.
-
Local Fallback: When the remote server is unreachable (VPN off, network issues):
- Automatically falls back to local execution
- Shows macOS notification on state changes
- Reminds you every 5 minutes if still offline
- Seamlessly switches back when remote is available
We tried SSHFS (remote filesystem mounting) first, but switched to Mutagen for several reasons:
| Aspect | SSHFS (mounting) | Mutagen (sync) |
|---|---|---|
| File access speed | Network latency on every operation | Instant (files are local) |
| Glob/Grep/Search | Slow - must traverse remote FS | Fast - operates on local files |
| Editor support | Works but can be laggy | Full native speed |
| Offline work | Broken when disconnected | Files available locally |
| Large repos | Sluggish with many files | Only syncs what changed |
The tradeoff is disk space (files exist in both places) and sync delay, but in practice Mutagen syncs are near-instant and the UX is dramatically better.
- macOS (tested on macOS 15+)
- Claude Code installed
- SSH access to a remote server (with key-based auth)
- Mutagen for file synchronization
# Install Mutagen
brew install mutagen-io/mutagen/mutagengit clone https://github.com/langwatch/claude-remote.git ~/Projects/claude-remote
cd ~/Projects/claude-remote
./setup.shThe setup script will:
- Prompt for your remote server details
- Create symlinks in
~/bin - Test your SSH connection
# If you haven't set up SSH keys
ssh-copy-id ubuntu@your-server.com# Launch Claude with remote execution (syncs current directory)
claude-remoteOnce running, all Claude commands execute on the remote server:
❯ uname -a
Linux ip-10-0-3-248 6.14.0-1018-aws ... aarch64 GNU/Linux
❯ which pnpm python3
/home/ubuntu/.nvm/versions/node/v24.13.0/bin/pnpm
/usr/bin/python3
sync-start # Start Mutagen sync (auto-started by claude-remote)
sync-stop # Stop Mutagen sync
sync-status # Check sync statusEdit config.sh (created by setup.sh):
REMOTE_HOST="ubuntu@your-ec2-instance.amazonaws.com"
REMOTE_MIRROR_ROOT="/home/ubuntu/claude-remote-mirror"
PATH_IDENTITY="false" # set to "true" for identity-path mode (see below)When PATH_IDENTITY="true", files are synced to the identical absolute path on the remote box instead of being mirrored under REMOTE_MIRROR_ROOT. For example, /Users/USER/workspace/myproject on the Mac syncs to /Users/USER/workspace/myproject on the box — no path translation at all.
Benefits: git worktrees just work (no .git file rewriting needed), and any tool that embeds absolute paths in its output is correct on both sides without translation.
Requirements: the box must have /Users/<localuser> created and owned by the ssh user. setup.sh does this automatically when you answer y to the PATH_IDENTITY prompt. To do it manually:
# On the box (as a user with sudo):
sudo mkdir -p /Users/<localuser> && sudo chown ubuntu:ubuntu /Users /Users/<localuser>/Users on the box is a plain directory — no bind-mount or special setup needed.
Note: only the directory you run claude-remote from is synced. One mutagen session is created per launch directory. Paths outside that tree are not present on the box.
Toggling is driven by a companion /claude-remote skill that lives in your Claude Code skills directory at ~/.claude/skills/claude-remote/ (it ships separately from this repo). Once that skill is present, toggle from inside any Claude Code session:
/claude-remote on # write per-session mode=on
/claude-remote off # write per-session mode=off
/claude-remote status # print resolved mode, source, and url
Because remote-shell.sh is re-executed on every Bash tool call, the toggle takes effect on the next command — no Claude restart needed.
Scope default is per-session: on/off write a per-session file (not global). This means toggling on in one session does not affect other sessions. To set a persistent default, write the global file directly: printf 'on\n' > ~/.claude-remote/mode.
How the toggle works (file-backed): Claude Code re-execs $SHELL per Bash call, so environment variables set by a child shell are lost by the next call. The toggle writes a file that is re-read on every call instead:
~/.claude-remote/session/<id> — per-session on|off (highest precedence)
~/.claude-remote/mode — global on|off
~/.claude-remote/url — ssh host string (e.g. ubuntu@10.0.3.56)
Mode precedence (highest first):
- Per-session file
~/.claude-remote/session/<CLAUDE_CODE_SESSION_ID> - Project-local file
$CLAUDE_PROJECT_DIR/.claude-remote-mode(ifCLAUDE_PROJECT_DIRis set) - Global file
~/.claude-remote/mode - Env var
$CLAUDE_REMOTE_MODE(launch-time seed — cannot change mid-session) - Built-in default:
on
Fail-safe: any value other than exactly on routes locally and emits a warning to stderr. A corrupt toggle file can never silently SSH to a box.
Wraps-not-activates: when claude-remote launches, if ~/.claude-remote/mode does not yet exist, the launcher seeds it with off. This means a fresh install is locally passthrough until you explicitly toggle on.
Project-local override: drop a .claude-remote-mode file in your project root (containing on or off) to override the global setting for that project only.
The env var is still honoured as a launch-time seed (precedence step 4 above). It cannot toggle mid-session because child-shell exports do not survive across Bash calls:
# These do NOT toggle mid-session (child export is gone next call):
export CLAUDE_REMOTE_MODE=off # ineffective after first Bash call
export CLAUDE_REMOTE_MODE=on
# Use the /claude-remote skill instead (inside a Claude session):
# /claude-remote off
# /claude-remote onEach claude-remote launch creates exactly one mutagen session for the launch directory. The session name is derived from the directory path (e.g. claude-remote--Users-localuser-workspace-myproject). Only that session is flushed before and after each remote command — other projects running in parallel are unaffected.
To clear zombie (halted/disconnected) sessions:
sync-reapThis lists all claude-remote-labelled sessions and terminates any whose status is Halted/Disconnected/errored or whose local alpha path no longer exists. Healthy sessions are left untouched.
Edit scripts/sync-start.sh to customize which files are excluded from sync:
--ignore="node_modules"
--ignore=".venv"
--ignore=".cache"
--ignore=".next*"
# ... add more patterns as neededIf your remote server runs services (dev servers, databases, etc.), forward ports to access them locally:
# Forward a single port (e.g., Next.js dev server on 3000)
ssh -N -L 3000:localhost:3000 ubuntu@your-server.com
# Forward multiple ports
ssh -N -L 3000:localhost:3000 -L 5432:localhost:5432 ubuntu@your-server.com
# Run in background
ssh -fN -L 3000:localhost:3000 ubuntu@your-server.comNow http://localhost:3000 on your Mac connects to the remote server.
For fastest startup, add your tools directly to ~/.profile on the remote machine instead of relying on slow shell initialization scripts like nvm.sh:
# On remote machine, add to ~/.profile:
export PATH=~/.nvm/versions/node/v24.13.0/bin:$PATH
export PATH=~/.local/bin:$PATH # for uv, pipx, etc.The remote shell sources ~/.profile. Ensure your PATH is set there:
- Check your remote
~/.profilehas the necessary paths - Verify:
ssh your-server "source ~/.profile && which pnpm"
# Check sync status
mutagen sync list
# Force flush sync
mutagen sync flush --label-selector=name=claude-remote
# Reset sync if stuck
sync-stop && sync-start# Test SSH
ssh -v your-server "echo ok"
# Clear SSH control socket if stuck
rm /tmp/ssh-claude-*If commands hang when your VPN reconnects or network changes, the SSH control socket may be stale. The script auto-detects this, but you can manually clear it:
rm /tmp/ssh-claude-*Claude Code invokes the shell like: $SHELL -c -l "command"
Our wrapper (scripts/remote-shell.sh):
- Checks if remote is reachable (with 2-second timeout)
- Falls back to local execution if not, with macOS notification
- Flushes Mutagen sync before running remote commands
- Maps local paths to remote paths in commands
- Handles Claude's working directory tracking (
pwd -P >| /tmp/...) - Forwards the command via SSH, sourcing
~/.profilefor PATH - Maps remote paths back to local in output
- Flushes Mutagen sync after command completes
- Preserves the exit code
PRs welcome! Some ideas:
- Support for Linux local machines
- Docker-based remote execution option
- Automatic port forwarding detection
- Sync conflict resolution UI
MIT