Every footgun we hit, with the working fix.
Homebrew's tailscaled doesn't install a resolver entry for the ts.net
domain. install.sh writes /etc/resolver/ts.net to fix this. If you've
already run the installer and it's still broken:
# Confirm the file exists and has the right content
sudo cat /etc/resolver/ts.net
# Expected:
# nameserver 100.100.100.100
# search_order 1
# timeout 5
# Verify macOS sees it
scutil --dns | grep -A4 'domain : ts.net'
# Should show nameserver[0] : 100.100.100.100
# Force-refresh
sudo dscacheutil -flushcachedig and host bypass macOS's resolver — they'll still return NXDOMAIN
even when everything is working. Verify with dscacheutil -q host -a name <fqdn> instead.
There's a second resolver file, /etc/resolver/search.tailscale, that
tailscaled generates on startup. It's a 52-byte stub for a pseudo
search domain — it looks dead but it's load-bearing for short-name
resolution. Don't delete it. If you already did:
sudo brew services restart tailscaleThat re-creates it.
sandboxed Tailscale GUI builds"
You have the App Store or macsys Tailscale build installed. Replace it with the open-source CLI build:
# Quit the GUI app first.
brew install tailscale
sudo brew services start tailscale
sudo tailscale up --sshIf tailscale debug prefs shows "RunSSH": true but nc -v <tailnet-ip> 22 returns "Connection refused", and tailscaled.log shows the daemon
finishing startup with no SSH-related lines at all, your tailnet ACL is
missing SSH rules.
Tailscale's SSH server only binds :22 on the tailnet IP if the
control-plane-delivered SSHPolicy contains at least one rule that
could apply to the node. Confirm with:
sudo tailscale debug netmap | python3 -c \
'import json,sys; d=json.load(sys.stdin); p=d.get("SSHPolicy") or {}; \
print("rules:", len(p.get("Rules",[])))'If that prints rules: 0, fix it in the ACL editor at
https://login.tailscale.com/admin/acls/file. A minimum rule that lets
the tailnet owner SSH into their own nodes as any non-root user:
{
"ssh": [
{
"action": "accept",
"src": ["autogroup:member"],
"dst": ["autogroup:self"],
"users": ["autogroup:nonroot"]
}
]
}tailscaled picks up the new policy within seconds — no restart needed.
This bites hard when you migrate from "plain SSH over Tailscale" (relies
on macOS Remote Login + OpenSSH on :22) to "Tailscale SSH server"
(replaces sshd). If Remote Login was masking the missing ACL, things
look fine until something turns Remote Login off — then :22 falls off
the network entirely. Symptoms of this transition:
ssh user@hostfrom a peer printsConnection refusedtailscale ssh user@hostfrom the same host returnsConnection refusednc -v 100.x.x.x 57739(peerapi) succeeds — proves the tun is uptailscaled.loghas zerohandling connorssh-conn-lines since startuptailscale debug netmapshows"cap/ssh"present butSSHPolicy.Rulesempty
Expected behavior on macOS — not a bug. ssh dfrysinger@macbook-air
from the same Mac that's hosting tailscaled gets routed through the OS
network stack (because macOS sees the tailnet IP as a local address on
the utun interface) instead of through tailscaled's netstack, so it
never reaches the Tailscale SSH server. Always test from a peer (your
phone, another laptop) — that's the real network path.
tmux needs Full Disk Access — see fda-grants.md. Symptom is specific
to tmux: ls ~/Library/CloudStorage/Dropbox works in a plain SSH shell
and fails inside tmux new-session.
tmux sessions are in-memory; they don't survive reboots. The wrapper
calls copilot --resume=<Name> on next launch, which restores the
Copilot CLI conversation, but tmux history (scrollback) is gone.
Answer "Yes". Reasoning: the Copilot CLI defaults to the macOS keychain,
which is unreliable over SSH — the keychain prompts the GUI for unlock,
which there's no human to dismiss, and the keychain remains locked. The
plaintext fallback lives at ~/.copilot/config.json with mode 0600,
inside the FileVault-encrypted home volume. The risk delta over the
keychain is small; the operational benefit is large.
--remote is correct (we tried --remote on first; that's a different
mode). The most common cause is the binary not being on PATH inside
tmux. Sanity check from inside an active tmux session:
which copilot
echo "$PATH"If which copilot is empty, either install Copilot CLI to a standard
PATH location or add its install dir to ~/.zshenv so non-login shells
see it. ~/.zshrc is not enough — tmux launches non-login,
non-interactive shells for send-keys.
The wrapper calls security unlock-keychain once before starting the
new tmux session. That unlocks the login keychain for the whole user
session, so anything launched inside tmux (gh, git, ssh-add, the
AWS CLI, Slack tokens, etc.) can read its stored secrets normally.
If the keychain re-locks later (e.g., the machine sleeps, or you have a
short idle-lock policy), just run security unlock-keychain once inside
tmux. Type your login password and you're back in business.
Symptom: tapping a host on Termius iOS pops a "Username / Password / Key" prompt mid-connection, and submitting blanks gives "connection failed."
Cause: the username on the host config is empty or doesn't match a
real macOS user. Tailscale SSH authenticates by tailnet identity, but
the username still has to map to a real OS user the ACL allows
(autogroup:nonroot covers any non-root user).
Fix: set the username on the host (or its Mac group) to the value
whoami prints on the Mac. The password field can stay blank —
Tailscale SSH ignores it.
Termius's iOS clipboard handler occasionally injects ^@ (null) bytes
into pasted content. Symptom is commands silently failing or only
running the first line of a heredoc.
This was the original motivation for replacing the multi-line shell
snippet with the single-line copilot-agent <Name> wrapper. If you
still need to paste something multi-line from your phone, put it in a
text file (TextEdit, Dropbox-synced) and cat it on the Mac instead.
This was the original reason for the now-removed watchdog daemon. If
you see this again on this stack, don't bring the watchdog back —
it fought with MDM and lost. Switch to Tailscale SSH (sudo tailscale up --ssh) which is what this stack uses, and let macOS's built-in Remote
Login stay off.