Claude Code in My Pocket: Remote Access, Built by Hand
From an iPhone to Claude Code on a MacBook through Tailscale, hardened OpenSSH, mosh and tmux — set up one step at a time, gotchas included.
tau is a personal agent platform I am building for myself: a hub running on my MacBook that I talk to from Telegram, from a terminal and by voice. I am building it layer by layer, and the bottom one, layer zero, has nothing to do with the agent at all. Its only job is letting me use Claude Code — Anthropic's coding agent that runs in the terminal — on the Mac from my phone, so the work does not stop when I leave the desk.
The repo already had scripts that set this up, and the part that needs no sudo, like tmux and mosh, was already installed. I still did the rest, everything that needs a password, an account or the phone, by hand and one step at a time instead of running the script: I cannot defend a door I set up without understanding it. Most of what I learned came from the gotchas along the way.
The chain
Termius on the iPhone → Tailscale → OpenSSH on the Mac → mosh → tmux → Claude Code. Each link has one job.
The road: Tailscale
Tailscale is a VPN that connects your devices as if they were on the same local
network, wherever they actually are; that private network is called a
tailnet. No ports need opening on the router. I installed it on the Mac with
brew install --cask tailscale-app, allowed the system extension under Privacy
& Security, and logged in. Then I installed it on the iPhone and logged in with
the same account.
The first confusion came right away: I thought I had opened Tailscale, and I had opened Termius. Termius is the SSH client I use on the iPhone; SSH is the standard way to open a secure terminal on another machine. The analogy that separated the two in my head: Tailscale is the road, Termius is the car. Tailscale lays a road between the phone and the Mac, but nothing travels on it by itself; the thing that drives to the Mac is Termius.
To see from the Mac that the road really exists:
tailscale ping <phone>
The answer came back as via <lan-ip>:41641: the two devices found each other
on the same Wi-Fi, and the traffic goes directly, peer to peer. Had it said
via DERP(...), the traffic would have been going through DERP, Tailscale's
relay servers. Later I tried it over 5G, and it worked there too.
The key, and the command on the clipboard
For SSH I use a key pair instead of a password: the private key stays on the
phone, the public key goes to the Mac, and the Mac only lets in whoever holds
that private key. In Termius I generated an ED25519 key under Keychain →
Generate Key and added the public key to ~/.ssh/authorized_keys on the Mac:
mkdir -p ~/.ssh && chmod 700 ~/.ssh
echo "$(pbpaste)" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
Then I checked:
ssh-keygen -l -f ~/.ssh/authorized_keys
# ... is not a public key file.
When I looked with cat, the file did not contain the key; it contained the
command itself.
echo 'ssh-ed25519 AAAA… iphone-termius' > ~/.ssh/authorized_keys
A single >, so the broken line goes too. The trailing iphone-termius is a
comment: SSH does not care about it, but if the phone gets lost it shows at a
glance which line to delete. This time ssh-keygen -l printed the key's
fingerprint (SHA256:…).
Lock the door before opening it
On macOS the SSH server, OpenSSH's sshd service, is called Remote Login in
System Settings. I hardened it before turning it on; I did not want it open with
default settings for even a minute.
/etc/ssh/sshd_config contains the line Include /etc/ssh/sshd_config.d/*: the
files in that folder are read in alphabetical order, and for most options the
first value seen wins. macOS puts its own settings in 100-macos.conf. That
is why mine is 010-tau.conf: it is read first, so it gets the first word.
I rendered the file from the repo's template with sed and put it in place with
install:
sed -e 's|__USER__|<user>|g' \
-e 's|__TAILNET_V4__|100.64.0.0/10|g' \
-e 's|__TAILNET_V6__|fd7a:115c:a1e0::/48|g' \
infra/remote/sshd/tau.conf > 010-tau.conf
sudo install -m 644 -o root -g wheel 010-tau.conf /etc/ssh/sshd_config.d/010-tau.conf
install, because sshd refuses a config file that is group- or world-writable,
and install sets the owner and the permissions right in one step. The heart
of it is these lines:
PasswordAuthentication no
KbdInteractiveAuthentication no
AuthenticationMethods publickey
PermitRootLogin no
AllowUsers <user>@100.64.0.0/10 <user>@fd7a:115c:a1e0::/48 <user>@127.0.0.1 <user>@::1
ClientAliveInterval 30
ClientAliveCountMax 4
AllowAgentForwarding no
AllowTcpForwarding local
- Passwords are closed in two places.
PasswordAuthentication nois not enough on its own: macOS ships withUsePAM yes, and PAM, the system's authentication layer, can still ask for the password through the keyboard-interactive route. That is whyKbdInteractiveAuthentication nois required, andAuthenticationMethods publickeylocks the door to a single method. - The source restriction lives in
AllowUsers. On macOS, sshd is started by launchd (the macOS service manager) on every interface, andListenAddresshas no effect. So the restriction goes here: only my user, only from the tailnet and from the machine itself.100.64.0.0/10is the CGNAT range Tailscale hands out addresses from (a shared block that is not routed on the internet);fd7a:115c:a1e0::/48is its IPv6 counterpart. Someone at the café can reach port 22 and still not be let in. - Dead connections get cleaned up.
ClientAliveInterval 30×ClientAliveCountMax 4: a probe every 30 seconds, closed after four unanswered ones. Half-open connections left behind by a sleeping phone are gone in ~2 minutes. - Forwarding is narrow. Agent forwarding is off; for TCP only local (
-L) forwarding is allowed, which is enough to reach a dev server on the Mac from the phone.
"no hostkeys available"
After installing it, and before turning Remote Login on, I tested it:
sudo sshd -t
# sshd: no hostkeys available -- exiting.
The key here is not my key. In SSH both sides prove who they are. The user key lets me tell the Mac "this is me"; that is the one I generated in Termius. The host key is the server's identity: it proves to me that the thing I connected to really is my Mac. Remote Login had never been turned on on this Mac, and since macOS normally creates the host keys on the first connection, there were none at all.
sudo ssh-keygen -A
-A only generates the host keys that are missing and leaves existing ones
alone. After that, sshd -t passed silently. The repo's setup script had the
same gap, so I fixed that too.
The host key is useful for one more thing: on the first connection Termius shows its fingerprint and asks whether you trust it. Instead of saying "yes" blindly, compare it with the value on the Mac; if the two match, the machine you connected to really is that Mac:
ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub
Turning Remote Login on
sudo systemsetup -setremotelogin on
# ... requires Full Disk Access privileges.
Giving the terminal Full Disk Access means opening the whole disk to everything that runs in that terminal; too much for a single setting. I turned Remote Login on by hand in System Settings → General → Sharing.
Two more doors left open
The real surprise was on that Sharing screen. Next to Remote Login, two more things were on:
- Remote Management: Apple Remote Desktop and screen sharing; ports 5900 and 3283.
- Remote Application Scripting: Apple Events from other machines, that is, controlling apps remotely by command; port 3031.
netstat -an -p tcp | grep LISTEN
These ports were listening on *, meaning every interface, café Wi-Fi
included. On top of that they accept the macOS login password, the hardening I
had just done does not cover them at all (it is sshd's config only), and the
firewall was off, because mosh runs over UDP. So anyone on the same café Wi-Fi
could have tried passwords against these services. I turned both off; of the
remote access doors, only 22 is left.
Health check
doctor.sh is the repo's health check; it probes the whole chain without
changing anything. My favourite check: it tries to log in to its own machine
with a password, and only believes that passwords are really off if the server
says nothing but Permission denied (publickey). Writing "no" in a config and
the server actually refusing are not the same thing. Everything came up green.
The car: Termius and mosh
A new host in Termius:
- Hostname: the Mac's full MagicDNS name,
<mac>.<tailnet>.ts.net. MagicDNS is the name Tailscale gives every device, so there is no IP to memorise. - Username: my user name on the Mac.
- Key: the key I generated in Termius; no password.
- Mosh: on.
mosh (mobile shell) uses SSH only to authenticate and to start mosh-server on
the Mac; after that the session flows over UDP. Because it is not tied to a
single TCP connection, switching from Wi-Fi to 5G or the phone going to sleep
does not kill it; when the phone wakes up, the screen carries on where it left
off.
tmux: the session lives on the Mac
mosh makes the connection resilient, but if I close the connection completely the terminal goes with it, and so does the Claude Code running inside. tmux is a terminal session that lives on the Mac independently of the connection: when I drop off, whatever runs inside keeps running, and when I reconnect I am back on the same screen.
The repo's tau-attach is essentially one line — attach to the tmux session
named tau, or create it:
exec tmux new-session -A -s tau
I typed it by hand from the phone first, and tmux's status bar appeared at the
bottom of the screen. Then I put tau-attach in the host's Startup Snippet
field in Termius; now every connection lands straight in the running session.
The test
I connected over 5G and typed claude. It did not ask me to log in again. (In
case the keychain, the macOS password vault, looks locked in an SSH session and
Claude Code asks to log in, the README documents a long-lived token route with
claude setup-token; I did not need it.)
The real test: lock the phone and wait more than two minutes, longer than it takes sshd to clean up dead connections. When I came back, the same screen. Then I closed the connection completely and reconnected: still the same, still-running Claude session.
The one gap: sleep
When the lid closes the Mac goes to sleep, and nothing can reach a sleeping Mac.
For now a launchd agent runs caffeinate -s: caffeinate is the macOS command
that prevents sleep, and with -s it only applies while on the charger; on
battery and with the lid closed the Mac still sleeps. Later, TauBar, the tray
app that will live in tau's menu bar, will take this job over.
Security model
- Network: no open port on the router; the way to the Mac goes through the tailnet.
- Who, from where: only my user, only from the tailnet and localhost
(
AllowUsers). - How: keys only; password, keyboard-interactive (PAM) and root logins are off.
- How far: agent forwarding is off, TCP forwarding is
-Lonly. - No other doors: with the firewall off for mosh, any service listening besides sshd is exposed; that is why Remote Management and Remote Application Scripting stay off.
- If the phone is lost: delete the
iphone-termiusline fromauthorized_keysand remove the device in the Tailscale admin console.
How to verify
tailscale ping <phone>answers:via <ip>:<port>means a direct connection,via DERP(...)a relayed one.ssh-keygen -l -f ~/.ssh/authorized_keysprints a fingerprint for every line and does not say "is not a public key file".sudo sshd -treturns without printing anything.ssh -o BatchMode=yes -o PubkeyAuthentication=no <user>@127.0.0.1 truesays onlyPermission denied (publickey).netstat -an -p tcp | grep LISTENshows no 5900, 3283 or 3031.- The host key fingerprint Termius shows on the first connection matches the
output of
ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub. - Connect from the phone, start
claude, lock the phone for more than two minutes: same screen. Close the connection completely and open it again: same session.
That is layer zero. I can now work with Claude Code when I am away from the Mac; next up is the agent itself.
If you want to set it up yourself: Claude Code from Your Phone: A From-Scratch Setup Guide.