Building a Remote Claude Code Dev Environment with Mac × WSL × Android
by 逆瀬川ちゃん
11 min read
Hi there! This is Sakasegawa (@gyakuse)!
Today I want to walk through how I built a remote dev environment that lets me drive Claude Code from three devices: a Mac, WSL on Windows, and Android. The combination of Ghostty, tmux, and happy-coder ended up working out really nicely, so I'll cover everything from the setup to the results of a security audit I ran on it.
The problem: dev environments are stuck on a single machine
When you're working with Claude Code, you often run into situations like "I want to check the current session from my phone" or "I want to pick up where I left off from another machine."
Concretely, scenarios like these:
- You started Claude Code on your Mac and are working away, but you'd like to be able to approve things from Android while out and about
- Your Windows machine has a GPU on it, so you'd like to run heavy workloads inside its WSL instance, but operate it from your Mac
- You'd like to fire off "go ahead and apply that fix" instructions from Android while sitting on the couch
In short, you want to decouple the dev session from any one device. Claude Code is a terminal app, so unlike a browser-based tool you can't just hit a URL to access it. The approach this time is to persist the session with SSH + tmux, and use happy-coder to operate it from mobile.
So let's look at the pieces we need to make this work.
The overall architecture
The final setup looks like this.
┌─────────────┐ SSH (Tailscale) ┌──────────────────────┐
│ Mac │ ──────────────────────→ │ WSL (Windows) │
│ Ghostty │ │ ┌─────────────────┐ │
│ │ │ │ tmux session │ │
└─────────────┘ │ │ └─ Claude Code │ │
│ └─────────────────┘ │
┌─────────────┐ E2E encrypted relay │ ┌─────────────────┐ │
│ Android │ ◄──────────────────────→│ │ happy daemon │ │
│ Happy App │ │ └─────────────────┘ │
└─────────────┘ └──────────────────────┘
Three things to note here.
- The Mac → WSL connection is plain SSH. With Tailscale you can reach across LANs as well
- A tmux session on WSL persists everything. Even if SSH drops, Claude Code keeps running
- Android talks to Claude Code through an end-to-end encrypted relay server
Let's go through each piece in turn.
Ghostty: a modern terminal that's good at SSH
For the terminal emulator, I'm using Ghostty. It's a GPU-accelerated terminal written in Zig by HashiCorp founder Mitchell Hashimoto.
I went with Ghostty because it's strong at SSH-based development, in two specific ways.
OSC 52 clipboard support
OSC 52 is a terminal escape sequence that lets a program on a remote machine copy text into the local system clipboard.
Why is that nice? Because when you select text inside tmux on a remote SSH host, it gets copied straight to your Mac's clipboard. No more manually re-copying things by hand. Ghostty supports OSC 52 natively, so it works without any configuration.
Automatic terminfo distribution
If your SSH target doesn't have Ghostty's terminfo, you'll get an error like missing or unsuitable terminal: xterm-ghostty. I tripped over that one myself early on.
Ghostty 1.2.0 added an SSH integration feature: if you put the following in your config, Ghostty will automatically push terminfo to remote hosts when you SSH into them.
# ~/.config/ghostty/config
shell-integration-features = ssh-terminfo,ssh-env
ssh-terminfo does the auto-distribution, and ssh-env sets up a TERM fallback (it falls back to xterm-256color if installing terminfo fails).
If you want to do it by hand, export terminfo on the Mac side and ship it over.
# On the Mac
TERMINFO=/Applications/Ghostty.app/Contents/Resources/terminfo \
infocmp xterm-ghostty > /tmp/ghostty.terminfo
scp /tmp/ghostty.terminfo wsl:/tmp/
# On the WSL side
tic -x /tmp/ghostty.terminfo
After this, xterm-ghostty works on the remote, with proper 256 colors plus modern text styling like italics and undercurl.
OK, now that the terminal is set up, let's move on to session persistence.
tmux: session persistence and clipboard integration
tmux is a terminal multiplexer that keeps your session alive even when SSH disconnects. For long-running processes like Claude Code, it's essential.
Basic config
My ~/.tmux.conf on WSL looks like this.
# Enable mouse (pane selection, resize, scroll, text selection)
set -g mouse on
# OSC 52 clipboard integration
set -g set-clipboard on
# Modern terminal settings
set -g default-terminal "tmux-256color"
set -ag terminal-overrides ",xterm-ghostty:RGB"
A walk-through of each line.
set -g mouse on enables mouse support. With just this, you get clickable pane selection, draggable border resizing, scroll wheel, and text selection.
set -g set-clipboard on is the OSC 52 hookup. When you yank text in tmux's copy-mode, it goes into the tmux buffer and an OSC 52 escape sequence is also passed through to the parent terminal (Ghostty), which copies it into the Mac's system clipboard. That's why you can select text inside tmux on a remote host and paste it locally with Cmd+V.
For default-terminal, use tmux-256color. People used to set screen-256color, but tmux-256color supports modern features like italics and RGB colors, and is the recommended choice today.
The last terminal-overrides line enables Ghostty's RGB (truecolor) support.
tmux-256color vs screen-256color
The difference is subtle but matters, so here it is in a table.
| Item | tmux-256color | screen-256color |
|---|---|---|
| Italic text | Supported | Not supported |
| Key sequence recognition | Recognizes more sequences | Some not recognized |
| Portability | Needs a recent ncurses | Works in older environments |
| Recommendation | Recommended on modern systems | For legacy environments |
On WSL or any modern Linux distro, tmux-256color is fine. On something like an older CentOS, fall back to screen-256color.
A dev-tmux script
I wrote a small script for managing the dev session on WSL.
#!/usr/bin/env bash
# ~/bin/dev-tmux: start/manage the WSL dev environment's tmux session
set -euo pipefail
SESSION_DEV="dev"
start_session() {
# Check happy daemon status (for Android integration; auto-started by `happy`)
if command -v happy &>/dev/null; then
happy daemon status 2>/dev/null || true
fi
# Create the tmux session
if ! tmux has-session -t "$SESSION_DEV" 2>/dev/null; then
tmux new-session -d -s "$SESSION_DEV" -c "$HOME"
echo " [dev] started"
else
echo " [dev] already running"
fi
}
attach_session() {
start_session
tmux attach-session -t "$SESSION_DEV"
}
stop_session() {
tmux kill-session -t "$SESSION_DEV" 2>/dev/null && echo " [dev] stopped" || true
if command -v happy &>/dev/null; then
happy daemon stop 2>/dev/null || true
fi
}
status() {
echo "=== tmux ==="
tmux ls 2>/dev/null || echo " no sessions"
echo "=== happy daemon ==="
happy daemon status 2>/dev/null || echo " not running"
}
case "${1:-start}" in
start) start_session ;;
attach) attach_session ;;
stop) stop_session ;;
status) status ;;
*) echo "Usage: dev-tmux {start|attach|stop|status}" ;;
esac
To call it from the Mac over SSH:
# Start a session (SSH + attach)
ssh wsl -t 'dev-tmux attach'
# Just check status
ssh wsl 'dev-tmux status'
There's one gotcha here. WSL's .bashrc typically has its PATH configuration written after the # If not running interactively, don't do anything guard. With a non-interactive call like ssh wsl 'dev-tmux status', PATH never gets set up and you'll hit command not found. The fix is to add the PATH line before the interactive guard in .bashrc.
# ~/.bashrc (near the top, before the case statement)
export PATH="$HOME/bin:$PATH"
At this point you have a stable way to reach Claude Code on WSL from your Mac. Next up, accessing it from Android.
happy-coder: drive Claude Code from your phone
happy-coder is a mobile/web client for Claude Code. It lets you operate Claude Code sessions from a smartphone app. The OSS project has about 5.7k GitHub stars.
Install and connect
# Install on WSL
npm install -g happy-coder
# Start a session (a QR code shows up)
happy
The first time you run happy, it shows a QR code in the terminal — scan it with the Happy Coder app on Android. That's pairing done.
Daemon auto-start
Running the happy command auto-starts the daemon in the background. You don't need to call happy daemon start explicitly.
# Check status
happy daemon status
# Stop the daemon
happy daemon stop
Once the daemon is running, opening the Android app is enough to reach the Claude Code session on WSL. You don't need to run happy inside tmux — the daemon listens for Android connections independently.
What you can do
From the Android app you can:
- Send messages to Claude Code (text or voice input)
- Approve/reject tool execution (delivered as push notifications to your phone)
- Switch sessions (manage multiple Claude Code sessions)
- Specify a working directory
When you're out and about, push notifications like "Bash execution requires approval" come in, you check the contents, and one tap approves it. This turns out to be surprisingly handy: you can have Claude Code chew on a long task while you move around.
Security: how E2E encryption is implemented
happy-coder implements end-to-end encryption — by design, the relay server can't read chat contents. I actually audited the source to confirm.
The encryption setup looks like this.
- Devices exchange public keys out-of-band via the QR code
- Key exchange uses tweetnacl (Curve25519 + XSalsa20 + Poly1305)
- Session data is encrypted with AES-256-GCM (authenticated encryption) before transmission
- The relay server just stores and forwards the encrypted bytes as-is
Because there's a physical step of scanning a QR code, this is also resistant to network-level man-in-the-middle attacks. It's similar in spirit to verifying an SSH key fingerprint.
Credentials are saved to ~/.happy/agent.key as JSON. The file permissions are set to 0600 (owner read/write only), so even on a multi-user machine other users can't read it.
Security: things to watch out for
The audit also surfaced a few concerning items.
| Risk | Details |
|---|---|
| Critical | The vendor API key registration feature (OpenAI etc.) isn't E2E — keys are encrypted on the server side |
| High | The local daemon's HTTP server (bound to 127.0.0.1) has no authentication |
| High | A DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING env var exists, which sends debug logs to the server |
| Critical | The mobile app can send permissionMode: 'yolo', bypassing Claude Code's permission approval |
| High | customSystemPrompt / appendSystemPrompt get passed to the Claude CLI without sanitization |
The permission-related one is a design-level concern in particular. If a mobile device gets compromised, an attacker could bypass Claude Code's safety controls and run arbitrary commands.
That said, a realistic risk assessment should consider the following.
- An attack requires compromising a paired mobile device first (E2E encryption rules out network sniffing)
- Tampering control messages from the relay server isn't possible either (the meta field is part of the encrypted payload)
- For personal dev use, if you're managing your own phone properly the practical risk is low
Recommended mitigations:
- Don't use the vendor API key registration feature (
happy connect). Manage API keys via local environment variables instead - Maintain device security on your Android (screen lock, don't install sketchy apps)
- If you want to self-host happy-coder's server, Happy Server is open source
While we're here, let me cover an alternative tool too.
Alternative: hapi
hapi is an OSS project being developed as an alternative to happy-coder, with about 1.5k GitHub stars. The design philosophy is different, so let's compare them.
| Item | happy-coder | hapi |
|---|---|---|
| Design philosophy | Cloud-hosted, multi-user | Local-first, single-user |
| Communication encryption | E2E (via relay server) | WireGuard + TLS (direct connection) |
| Security model | Untrusted server assumed | You manage the server yourself |
| Deployment | npm install + scan QR in app | One-command, but needs network setup |
| Supported AIs | Claude Code, Codex, Gemini CLI | Claude Code, Codex, Gemini, OpenCode |
| License | MIT | AGPL-3.0 |
What stands out about hapi is its WireGuard-based direct connection. There's no relay server in between; devices connect directly to each other. If you're already on Tailscale, you can combine it as the underlying hub.
happy-coder, on the other hand, is easier to start using casually. npm install plus QR scan and you're done. It depends on a relay server, but since it's E2E encrypted the server can't read anything.
I'm on Tailscale myself so hapi probably plays well with my setup, but happy-coder hasn't given me any trouble so I keep using it. If you don't want to mess with network settings, happy-coder is the easier choice.
Putting it together: my actual dev flow
Let me show you how I actually use these tools together day-to-day.
Working from the Mac
# 1. SSH to WSL and attach to the tmux session
ssh wsl -t 'dev-tmux attach'
# 2. Inside tmux, start Claude Code
claude
# 3. When done, detach tmux (Ctrl+B, D)
# Claude Code keeps running in the background
# 4. Reattach later
ssh wsl -t 'tmux attach -t dev'
Using tmux pane splits, you can run multiple Claude Code sessions in parallel.
# Inside tmux, split horizontally
# Ctrl+B, "
# In the new pane, start Claude Code in another directory
cd ~/src/github.com/nyosegawa/another-project
claude
Working from Android
1. Open the Happy Coder app
2. The happy daemon on WSL is auto-discovered
3. Pick the Claude Code session you want to operate from the list
4. Send a message or approve a tool call
A common usage pattern: "Start a big refactor on the Mac → leave the house → check progress and approve from Android → come home and check the result on the Mac." When Claude Code asks for tool execution approval, you get a push notification, so work doesn't stall while you're moving around.
Session management tips
Claude Code has a --resume option, so you can pick up an interrupted session.
# Resume the most recent session
claude --continue
# Resume a specific session by ID
claude --resume <session-id>
# Pick from a list of sessions
claude --resume
Combining tmux's session persistence with Claude Code's session resume means you don't lose Claude Code's context even if SSH disconnects.
Why develop on WSL?
You might wonder: "Why not just run Claude Code locally on the Mac?" Two reasons.
First is the GPU. The Windows machine has an NVIDIA GPU, and I want to use it via WSL when running local LLMs or CUDA-based tools.
Second is session persistence. Claude Code on the Mac dies when you close the terminal, but tmux on WSL keeps running indefinitely. Even if I put my Mac to sleep, the session stays alive. The happy daemon is also resident on WSL, so I can hit it from Android any time.
Ghostty + tmux config summary
Finally, here are the config files all in one place. They're meant to be pasted in directly.
Mac side: Ghostty config
# ~/.config/ghostty/config
shell-integration-features = ssh-terminfo,ssh-env
WSL side: tmux config
# ~/.tmux.conf
set -g mouse on
set -g set-clipboard on
set -g default-terminal "tmux-256color"
set -ag terminal-overrides ",xterm-ghostty:RGB"
WSL side: .bashrc PATH setup
# ~/.bashrc (added before the interactive guard)
export PATH="$HOME/bin:$PATH"
# If not running interactively, don't do anything
case $- in
*i*) ;;
*) return;;
esac
WSL side: happy-coder setup
# Install Node.js (fnm recommended)
curl -fsSL https://fnm.vercel.app/install | bash
fnm install --lts
# Install happy-coder
npm install -g happy-coder
# First-time auth (shows a QR code; daemon auto-starts too)
happy
Summary
- The Ghostty + tmux + OSC 52 combo gives you a modern remote dev experience over SSH, including clipboard sharing
- happy-coder's E2E encryption is implemented properly, but the API key registration feature and the permissionMode operation are design-level concerns. For personal use, device hygiene covers it
- The alternative, hapi, uses WireGuard-based direct connections. Especially nice if you're already on Tailscale
Appendix: detailed happy-coder security audit
Details for the security findings touched on in the main text. Based on a full source-code analysis using Gemini 3 Pro.
E2E encryption implementation
- Key exchange: tweetnacl.box (Curve25519 + XSalsa20 + Poly1305)
- Data encryption: AES-256-GCM (authenticated encryption)
- Key exchange happens out-of-band via QR code. Stealing keys via network eavesdropping isn't possible
- Metadata (permissionMode, appendSystemPrompt, etc.) is also part of the E2E payload, so the relay server can't tamper with it
Local daemon design
- Bound to
127.0.0.1(not reachable from external networks) - The HTTP server has no authentication. Other processes on the same machine can
POST http://127.0.0.1:<port>/spawn-sessionto create a session in any directory - Practically low risk in a personal WSL environment, but be careful on shared machines
Instruction injection
customSystemPrompt/appendSystemPrompt: if included in the meta field of a message from mobile, they're passed unsanitized to Claude CLI's--system-prompt/--append-system-promptargumentspermissionMode: sending'yolo'translates to--permission-mode bypassPermissions, which skips user approval at tool execution- MCP server injection isn't possible (there's no logic to set mcpServers from the meta field)
- On Unix systems,
spawnis called with array form, so shell injection doesn't happen (but be careful: there are spots on Windows whereshell: trueis used)
Credential storage
- Location:
~/.happy/agent.key(JSON format) - Directory: permissions 0700
- File: permissions 0600
- The private key is base64-encoded but not encrypted (same model as an SSH private key)
References
- Ghostty
- tmux
- happy-coder
- hapi
- Claude Code
- Networking