
Moshi with Tailscale
Reach a Mac or Linux box with no public IP from anywhere — put both devices on a tailnet, Easy Pair over it with a rotation-proof MagicDNS address, and understand exactly when Tailscale SSH helps and when it silently breaks key auth.
The machine you want your agents on is usually the one with no public IP: the Mac mini in the closet, the desktop at home, the homelab box behind your router's NAT. Tailscale fixes that without opening a single port — every device you sign in gets a stable private address on your tailnet, reachable from anywhere, end-to-end encrypted over WireGuard.
Moshi needs no integration for this, and that's the point. Tailscale runs at the OS layer on iOS; Moshi just sees a host it can reach. SSH, mosh, key auth, password auth — everything behaves exactly as it would on your living-room Wi-Fi, except now "your living room" follows you onto the train.
Mental model. A tailnet is your LAN, everywhere. Moshi treats tailnet hosts as ordinary SSH or mosh targets — there is no Tailscale toggle anywhere in Moshi, and the tunnel is brought up or down in the iOS Tailscale app, not here.
What you'll learn
- Why a tailnet beats port forwarding and a rented VPS for agent hosts
- Put the host and your phone on the same tailnet, and pick the right address form
- Easy Pair over Tailscale — and why the QR bakes in your MagicDNS name
- Tailscale SSH vs SSH over Tailscale: the fork that causes the infamous 60-second hang
- Run mosh through the tunnel, and what happens when a Mac host sleeps
- Debug the handful of failures that look like Moshi problems but aren't
Why a tailnet
There are three common ways to reach a home machine from a phone. Two of them have sharp edges.
For a personal agent host, the tailnet wins on every axis that matters: no inbound firewall holes, no dynamic-DNS dance when your ISP rotates your IP, and the same address works from home Wi-Fi, cellular, and hotel networks alike.
Part 1 — Put both ends on the tailnet
Install Tailscale on the host and sign in, then do the same on your phone with the same account:
# macOS$brew install tailscale$sudo tailscale up# what to put in Moshi's Host field$tailscale ip -4$tailscale status

Three address forms work in Moshi's Host field, in rough order of preference:
- MagicDNS name —
mac-minior the fullmac-mini.tail-scales.ts.net. Names survive IP rotation, so this is the most durable choice. - Tailscale IPv4 — the
100.x.y.zaddress fromtailscale ip -4. - Tailscale IPv6 — the
fd7a:115c:…address, if that's how you roll.
Do not use the host's LAN IP (192.168.x.x), its public IP, or the Tailscale web SSH proxy URL. Moshi connects directly through the OS-level tunnel — anything that isn't a tailnet address bypasses it.
Part 2 — Easy Pair over Tailscale
You could now add a connection by hand, but Easy Pair does it better — and it's Tailscale-aware. On the host:
$moshi-hook host setupfound tailscale: mac-mini.tail-scales.ts.net (100.84.2.17)
Before printing the QR, moshi-hook host setup probes the local tailscale CLI. If the daemon is running, it bakes your tailnet address into the QR — preferring the MagicDNS name over the raw IP, because names survive address rotation. No Tailscale? It falls back to a LAN address or the Mac's Bonjour .local name. You don't have to know which address form to pick; the host already did.
Scanning the QR from Moshi's onboarding then does the rest:
- Checks the host's prerequisites — Remote Login on,
tmuxandmosh-serverreachable over a non-interactive shell - Generates an Ed25519 keypair on the phone — the private key never leaves the device
- Sends only the public half to the host's setup session, which installs it in
~/.ssh/authorized_keys - Saves the connection with the tailnet address, key auth, and transport on Auto
Treat the Easy Pair QR like a temporary access token. Anyone who scans it before it expires can claim SSH access to that host.
One more thing the setup command does for you: if it detects Tailscale SSH enabled on the host, it refuses to pair and tells you why. That deserves its own section, because it's the single most confusing failure mode in this whole setup.
Part 3 — Tailscale SSH is not SSH over Tailscale
Two features sound alike and work in completely different ways. Mixing them up produces a connection that hangs for about a minute and then fails with a misleading auth error — with a key you know is authorized.
- SSH over Tailscale — everything this guide has described so far. Moshi connects to the host's normal OpenSSH server (
sshd) through the tunnel. Keys, passwords, SSH, and mosh all behave exactly as on a LAN. This is the path Easy Pair sets up. - Tailscale SSH — a separate, Tailscale-managed SSH server inside
tailscaled, enabled withtailscale set --ssh=true. When it's on, Tailscale grabs port 22 for every tailnet connection and authenticates with your Tailscale identity — bypassing the OSsshdandauthorized_keysentirely.
The failure looks like this: Easy Pair (or you) installed a key in authorized_keys, but with Tailscale SSH on, traffic to port 22 never reaches the OS sshd — so the key is never even checked. The connection stalls for ~60 seconds, then reports a generic auth error. The tell-tale sign: the host is reachable on every port except the one that matters — a dev server on 8080 loads instantly over the same tailnet address while port 22 hangs.
Pick one path
Key auth (recommended). Hand port 22 back to the OS sshd:
$sudo tailscale set --ssh=false$moshi-hook host setup
Key auth then works for Moshi and every other SSH client, mosh can bootstrap, and Easy Pair runs without complaint. This is the simpler, more reliable choice for an agent host — it's why moshi-hook host setup checks tailscale debug prefs for RunSSH: true and stops you up front instead of letting you hit the 60-second hang later.
Tailscale SSH instead. If you deliberately run Tailscale SSH — say, your tailnet ACLs are how you govern access — Moshi can use it: leave the password and key fields empty in the connection. Tailscale authenticates through its identity layer, so Moshi has nothing to send (that's what the in-app hint about leaving the password blank refers to). Two hard limits:
- It only works with an accept-mode ACL. A check-mode rule (
"action": "check") demands a browser approval mid-handshake that a non-interactive client can't complete. - mosh cannot bootstrap through it — Tailscale SSH isn't real OpenSSH, so you're limited to plain SSH transport.
Part 4 — mosh through the tunnel
mosh works fine over Tailscale: the SSH bootstrap and the UDP session both ride the tunnel. Leave the connection type on Auto and Moshi prefers mosh with SSH fallback — which is what makes a tailnet host shrug off you walking out the door mid-session.
If mosh fails where SSH works, it's almost never Tailscale — the usual culprit is mosh-server missing from the non-interactive PATH. The troubleshooting doc has the checklist.
Agent events don't ride the tailnet at all. moshi-hook keeps its own outbound WebSocket to Moshi, so approvals and turn completions reach your phone even when the Tailscale app on iOS is off — you only need the tunnel up to open the terminal itself.
Part 5 — Tailnet hosts that sleep
A closed-lid Mac on the tailnet has one quirk worth knowing. mosh runs over UDP, and UDP traffic does not prevent macOS from drifting into low-power sleep — even with pmset tweaks — so an idle session can go "Unreachable (auto-retrying)" after a few minutes. Reconnecting is fast: Moshi sends a wake-up probe when starting a new mosh connection, which pulls the Mac back up.
If you want sessions to stay live without manual reconnects, block sleep at the source — lid open, caffeinate, or the full always-on treatment in Moshi with an always-on Mac.
Separately, Tailscale occasionally wants device re-authentication (key expiry, ACL changes). That prompt appears in the Tailscale app, not in Moshi — so if a host that worked yesterday is suddenly unreachable, check Tailscale first.
Troubleshooting
tailscale ping works but Moshi can't connect
Tailscale connectivity alone isn't enough — the host still needs SSH listening. On macOS, turn on System Settings → General → Sharing → Remote Login. Then confirm the tunnel from another tool (a desktop terminal, tailscale ping) before debugging Moshi settings.
Hangs ~60 seconds, then an auth error, with a key that's definitely authorized
That's Tailscale SSH hijacking port 22 — the key never reaches the OS sshd. Either sudo tailscale set --ssh=false (recommended) or clear the password/key fields and rely on tailnet identity. See Part 3.
"Unable to authorize"
The auth type doesn't match what the host expects — password vs key — even when Tailscale itself is fine. Match the Authentication setting in the connection to what the host's sshd actually accepts.
OS Error 4
The SSH handshake was interrupted. Re-check Remote Login, confirm you're using the host's tailnet address (not a stale LAN entry), and that the device is still signed in to the tailnet.
Where to go next
- Moshi with an always-on Mac — the host this network was made for
- Moshi with Claude Code — what to run once you're connected
- Tailscale and Connections — the reference docs behind this guide
- Install and prepare a host — Easy Pair from the beginning