Claude Code from Your Phone: A From-Scratch Setup Guide

The whole chain from an iPhone to Claude Code on an Apple Silicon Mac — Termius, Tailscale, hardened OpenSSH, mosh, tmux — copy-paste, with a check and a fix for every step.

This guide builds, from zero, the chain that lets you use Claude Code on your Mac from an iPhone: Termius → Tailscale → OpenSSH → mosh → tmux → Claude Code. Why I set it up this way, and the traps I fell into, are in Claude Code in My Pocket; this is just the recipe. Every step has what you are doing, the commands, how to check it and what goes wrong. You need no repo; it is all copy-paste.

the chain you will build · numbers are steps
iPhoneMactailnet · Tailscale · 2SSH · keymosh · UDPstartstarejectedTermiusSSH client · 6, 9sshdRemote Login · 7–8mosh-serverUDP · 3tmuxsession: main · 4–5Claude Codeclaude · 10Someone at a caféoff the tailnetSession2Gate1Client2Agent1

0. Before you start

1. Packages

tmux keeps the session alive on the Mac, mosh keeps the connection from dropping, and Tailscale puts the phone and the Mac on the same private network.

brew install tmux mosh
brew install --cask tailscale-app

Check: tmux -V and mosh --version each print a version.

2. Tailscale

Tailscale is a VPN that connects your devices as if they were on the same local network, wherever they are; that private network is called a tailnet. No ports need opening on the router.

The app's command-line tool is not on your PATH, so call it by its full path:

/Applications/Tailscale.app/Contents/MacOS/Tailscale status
/Applications/Tailscale.app/Contents/MacOS/Tailscale ping <phone>

If you like, add a shortcut to ~/.zshrc: alias tailscale=/Applications/Tailscale.app/Contents/MacOS/Tailscale.

The address you will give Termius is the Mac's full MagicDNS name. MagicDNS is the name Tailscale gives every device, in the form <mac>.<tailnet>.ts.net:

/Applications/Tailscale.app/Contents/MacOS/Tailscale status --json \
  | python3 -c 'import json,sys; print(json.load(sys.stdin)["Self"]["DNSName"].rstrip("."))'

Check: status lists both devices and ping answers. via <ip>:<port> in the answer means a direct connection, via DERP(...) means it goes through Tailscale's relay servers (DERP); both work.

If it goes wrong: tailscale: command not found → use the full path. If ping gets no answer, check that both devices are logged in with the same account and that the VPN is on on the iPhone.

3. PATH for SSH sessions

The sneakiest step in the guide. mosh starts mosh-server in a non-interactive SSH session; of zsh's startup files that session only reads ~/.zshenv, and Homebrew's /opt/homebrew/bin is not on the PATH there. The result: Termius says mosh-server not found.

cat >> ~/.zshenv <<'EOF'
typeset -U path
path=(/opt/homebrew/bin /opt/homebrew/sbin $HOME/.local/bin $path)
export LANG="${LANG:-en_US.UTF-8}"
EOF

The LANG line is required too: mosh-server refuses to run without a UTF-8 locale. The 'EOF' is quoted, so $HOME and $path are written to the file as they are and resolved every time a session starts. ~/.local/bin is for the script in the next step.

Check: does a clean, non-interactive shell find mosh-server?

env -i HOME="$HOME" /bin/zsh -c 'command -v mosh-server'
# /opt/homebrew/bin/mosh-server

If it goes wrong: empty output means the lines went into a file other than ~/.zshenv (~/.zshrc is not read by non-interactive sessions).

4. One command to attach: ta

tmux is a terminal session that lives on the Mac independently of the connection. ta attaches to the session called main, or creates it if it does not exist (that is what -A does), so every connection from the phone lands on the same screen.

mkdir -p ~/.local/bin
cat > ~/.local/bin/ta <<'EOF'
#!/bin/sh
exec tmux new-session -A -s main
EOF
chmod +x ~/.local/bin/ta

Check: open a new terminal and type ta: tmux's green status bar appears at the bottom. Detach with Ctrl-b then d; the session keeps living in the background.

If it goes wrong: ta: command not found → ~/.local/bin is not on the PATH; see step 3.

5. tmux settings for a phone

cat >> ~/.tmux.conf <<'EOF'
set -g mouse on
setw -g aggressive-resize on
set -g history-limit 50000
set -s escape-time 10
set -g default-terminal "tmux-256color"
set -g set-clipboard on
EOF

Check: if this command prints nothing, the config file has no errors:

tmux -L check -f /dev/null new -d \; source-file ~/.tmux.conf \; kill-server

If you already have a tmux session running, apply the settings with tmux source-file ~/.tmux.conf.

If it goes wrong: invalid option: … → there is a typo on that line.

6. SSH key

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 go to Keychain → + → Generate Key, type ED25519, and give it a name. Open the key and copy the public key (Universal Clipboard brings it straight to the Mac). On the Mac:

mkdir -p ~/.ssh && chmod 700 ~/.ssh
echo 'ssh-ed25519 AAAA…the-whole-key… iphone' >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

Paste the key inside the single quotes as literal text, and end it with a device name like iphone; if the phone gets lost, that tells you which line to delete.

Check:

ssh-keygen -l -f ~/.ssh/authorized_keys
# 256 SHA256:… iphone (ED25519)

If it goes wrong: is not a public key file → look with cat ~/.ssh/authorized_keys and fix the broken line with nano. sshd ignores a ~/.ssh or authorized_keys with loose permissions, so do not skip the two chmods.

7. Harden sshd

On macOS the SSH server, OpenSSH's sshd service, is called Remote Login in System Settings. We lock it down before turning it on: keys only, only your user, only from the tailnet.

U=$(whoami)
sudo tee /etc/ssh/sshd_config.d/010-remote.conf >/dev/null <<EOF
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
AuthenticationMethods publickey
PermitEmptyPasswords no
PermitRootLogin no
AllowUsers $U@100.64.0.0/10 $U@fd7a:115c:a1e0::/48 $U@127.0.0.1 $U@::1
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 30
ClientAliveCountMax 4
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding local
EOF
sudo chmod 644 /etc/ssh/sshd_config.d/010-remote.conf

This time EOF is unquoted, so $U turns into your user name as the file is written. If you see a literal $U in the file, the quoting slipped.

A Mac that has never had Remote Login on has no host keys, the keys that prove the server's identity, and the test says no hostkeys available. Generate the missing ones (existing ones are left alone), then test:

sudo ssh-keygen -A
sudo /usr/sbin/sshd -t

Check: if sshd -t prints nothing, the syntax is right. Look at the effective values:

sudo /usr/sbin/sshd -T | grep -iE '^(passwordauthentication|kbdinteractiveauthentication|permitrootlogin|authenticationmethods|allowusers)'
# permitrootlogin no
# passwordauthentication no
# kbdinteractiveauthentication no
# allowusers <user>@100.64.0.0/10
# allowusers <user>@fd7a:115c:a1e0::/48
# allowusers <user>@127.0.0.1
# allowusers <user>@::1
# authenticationmethods publickey

If it goes wrong: Bad configuration option → a typo in the file. If the values come out different, check that grep Include /etc/ssh/sshd_config still finds the line and that the file name starts with 010-.

8. Turn on Remote Login, close the other doors

sudo systemsetup -setremotelogin on wants the terminal to have Full Disk Access; rather than opening the whole disk to your terminal, use the GUI:

  1. System Settings → General → Sharing → Remote Login on.
  2. The (i) next to it: Allow full disk access for remote users off; Allow access for → Only these users → just your user.
  3. On the same screen, turn off Remote Management, Remote Application Scripting and Screen Sharing if they are on. They listen on every network interface, accept your macOS login password, and the hardening in step 7 does not cover them.

sshd restarts for every connection, so the settings apply immediately.

Check: of the remote access ports, only 22 should be open:

netstat -an -p tcp | grep LISTEN | grep -E '\.(22|5900|3283|3031) '
# tcp4  0  0  *.22  *.*  LISTEN
# tcp6  0  0  *.22  *.*  LISTEN

*.22 means listening on every interface; that is expected, AllowUsers does the filtering. Also make sure passwords are really off:

ssh -o BatchMode=yes -o PubkeyAuthentication=no \
  -o PreferredAuthentications=password,keyboard-interactive \
  -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
  "$(whoami)@127.0.0.1" true
# <user>@127.0.0.1: Permission denied (publickey).

If it goes wrong: if password or keyboard-interactive also shows up in the parentheses, the drop-in is not being read; go back to the check in step 7.

9. Termius

A new host in Termius:

mosh uses SSH only to log in and to start mosh-server; the session itself flows over UDP, so switching from Wi-Fi to 5G or the phone going to sleep does not kill it.

Then Snippets → +: name ta, command ta. In the host settings pick ta as the Startup Snippet; every connection lands straight in the main session.

On the first connection Termius shows the Mac's host key fingerprint and asks whether you trust it. Do not accept blindly; compare it with the value on the Mac:

ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub

Check: connect: if you see tmux's green status bar, the whole chain works.

If it goes wrong: see the troubleshooting table below.

10. Claude Code

While connected from the phone, type claude. Most of the time it just opens.

In an SSH session the login keychain, the macOS password vault, can look locked, and Claude Code may ask you to log in again. There are two ways out. Unlock it in each session (it asks for your Mac password):

security unlock-keychain ~/Library/Keychains/login.keychain-db

Or create a long-lived token and put it in a file that only SSH sessions read:

claude setup-token
mkdir -p ~/.config/remote && chmod 700 ~/.config/remote
printf 'export CLAUDE_CODE_OAUTH_TOKEN=%s\n' '<token>' > ~/.config/remote/env
chmod 600 ~/.config/remote/env
echo '[[ -n "$SSH_CONNECTION" && -r ~/.config/remote/env ]] && source ~/.config/remote/env' >> ~/.zshenv

Replace <token> with the value setup-token gives you. It is a secret: do not share the file and never commit it to any repo.

Check: in a new connection, echo ${CLAUDE_CODE_OAUTH_TOKEN:+set} → set, and claude opens without asking you to log in.

11. Optional: no sleep on the charger

Nothing can reach a sleeping Mac. caffeinate is the macOS command that prevents sleep; with -s it only applies on the charger. A LaunchAgent, a background job launchd starts when you log in, keeps it running:

mkdir -p ~/Library/LaunchAgents
cat > ~/Library/LaunchAgents/local.keepawake.plist <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>local.keepawake</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/bin/caffeinate</string>
    <string>-s</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>
</dict>
</plist>
EOF
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.keepawake.plist

Check: while on the charger:

launchctl print gui/$(id -u)/local.keepawake | grep 'state ='
# state = running
pmset -g assertions | grep PreventSystemSleep
# PreventSystemSleep  1

If it goes wrong: if it says state = not running, start it with launchctl kickstart gui/$(id -u)/local.keepawake. On battery and with the lid closed the Mac still sleeps; the only way to keep working with the lid closed is clamshell mode with an external display.

12. Final test

  1. Connect from the phone and start claude.
  2. Lock the phone, wait more than two minutes, come back: same screen.
  3. Close the connection in Termius completely and reconnect: the same, still-running session.

Troubleshooting

SymptomCauseFix
mosh-server not foundThe non-interactive SSH session does not see HomebrewStep 3
needs a UTF-8 native localeLANG is not setThe LANG line in step 3
Permission denied (publickey)Key not in authorized_keys, loose permissions, wrong user name, or the connection is not coming over the tailnetStep 6; Username = whoami; is Hostname the MagicDNS name, is Tailscale on on the iPhone
TimeoutTailscale is off on the iPhone or the Mac is asleepTurn Tailscale on; step 11
mosh connects but no screen appearsThe macOS firewall blocks mosh UDPAllow mosh-server with socketfilterfw (below)
tailscale: command not foundThe app CLI is not on the PATHFull path or alias, step 2
no hostkeys availableHost keys were never generatedsudo ssh-keygen -A
Claude asks to log inThe keychain is locked in the SSH sessionStep 10
ta: command not found~/.local/bin is not on the PATHStep 3

If the firewall is on, to allow mosh-server:

sudo /usr/libexec/ApplicationFirewall/socketfilterfw --add "$(realpath /opt/homebrew/bin/mosh-server)"
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --unblockapp "$(realpath /opt/homebrew/bin/mosh-server)"

Undo

sudo rm /etc/ssh/sshd_config.d/010-remote.conf
sed -i '' '/ iphone$/d' ~/.ssh/authorized_keys
launchctl bootout gui/$(id -u)/local.keepawake
rm ~/Library/LaunchAgents/local.keepawake.plist ~/.local/bin/ta
rm -r ~/.config/remote

Then turn Remote Login off in System Settings → General → Sharing, delete the lines you added to ~/.zshenv and ~/.tmux.conf by hand, and remove the phone in the Tailscale admin console.

Security model

The story behind these decisions, and how I learned each one, is in Claude Code in My Pocket.