Documentation

SSH

Open SSH sessions to a tailnet host through the profile that owns its hostname — with no system-wide Tailscale client.

View as Markdown

Quick answer#

zsh — tailmux
$tailmux ssh admin@host.work.ts.net

That's it. tailmux ssh opens a normal interactive SSH session, routed through whichever profile owns .work.ts.net. No flags to remember, no ProxyCommand to write, no profile to pick — the hostname decides the tailnet, and there is never a fallback to another profile.

For a service on a fixed local port (a database, RDP, anything a GUI client dials) use a tunnel instead — see shells vs services below.

tailmux ssh#

tailmux ssh wraps your system ssh. Everything after the command is handed to ssh unchanged, so all normal SSH usage keeps working:

zsh — tailmux
$# interactive shell$tailmux ssh admin@host.work.ts.net$ $# run a single remote command$tailmux ssh host.work.ts.net -- uptime$ $# bring your usual ssh flags — they pass straight through$tailmux ssh -L 9000:localhost:9000 host.work.ts.net

Hosts on different tailnets need no different treatment — the suffix picks the profile per invocation:

zsh — tailmux
$tailmux ssh admin@host.work.ts.net       # via the work profile$tailmux ssh admin@nas.home-lab.ts.net    # via the personal profile

tailmux ssh -h shows the wrapper's own help instead of launching ssh.

How it works#

tailmux ssh builds the ProxyCommand for you and then replaces itself with the real ssh process, so the terminal, signals, and exit status all belong to ssh — interactive shells, Ctrl-C, and TUIs on the remote host behave exactly as with plain ssh. Under the hood it runs:

zsh — tailmux
$ssh -o ProxyCommand="tailmux connect %h %p" <your args…>

tailmux connect <host> <port> opens a raw TCP stream over stdin/stdout through the profile that owns the host's suffix — exactly what SSH's ProxyCommand expects. SSH substitutes %h / %p with the target host and port at connect time.

Because it is your system ssh, your keys, ssh-agent, ~/.ssh/config, and known_hosts all apply as usual. Host-key verification is unaffected — you still confirm the remote host key exactly as you would without TailMux.

In the app: fixed SSH endpoints#

tailmux ssh is for interactive shells from a terminal. When something else needs to reach port 22 — an SFTP GUI, an IDE's remote workspace, a deploy tool that only accepts host:port — give it a fixed loopback endpoint with a tunnel:

  • Open the menu bar icon → OpenTunnels Add Tunnel.
  • Set the target to the host's SSH port, e.g. host.work.ts.net:22.
  • Leave the local endpoint blank — TailMux assigns a fixed free port, stable across restarts.
  • Copy the endpoint from the tunnel row and point the tool at it, e.g. 127.0.0.1:43301.

The tunnel stays up whenever the profile is running, with a start/stop switch and a live connection count in the menu bar. Tunnels can also forward through an SSH bastion (ssh -L run for you, auth via ssh-agent, a private key, or a password in the macOS Keychain) for targets only reachable from inside a remote network — the full details are on the tunnels page.

Plain ssh via ProxyCommand — no wrapper at all#

tailmux ssh is a convenience over a single SSH option. Put that option in ~/.ssh/config instead and everything that speaks SSH — plain ssh, scp, rsync -e ssh, Git remotes, an IDE's remote workspace, GUI SSH managers — routes through the owning profile automatically, with no tailmux prefix and no habit change. One-off, the option looks like this:

zsh — tailmux
$ssh -o ProxyCommand="tailmux connect %h %p" admin@host.work.ts.net

tailmux connect opens the raw TCP stream through the profile that owns the hostname; ssh does everything else. There are two good places to persist it — pick by how you manage your SSH config.

Option 1 — one wildcard Host per tailnet#

One block covers every host on a tailnet, present and future — nothing to add when a new machine joins:

~/.ssh/config
1Host *.work.ts.net2    ProxyCommand tailmux connect %h %p

With that in place, ssh admin@host.work.ts.net, scp file admin@host.work.ts.net:/tmp/, and git clone git@repo.work.ts.net:team/service.git all reach the work profile automatically. Using several tailnets? Add one block per suffix — each dials its own profile, and the no-fallback isolation between them is unchanged:

~/.ssh/config — one block per tailnet
1Host *.work.ts.net2    ProxyCommand tailmux connect %h %p3 4Host *.home-lab.ts.net5    ProxyCommand tailmux connect %h %p
A Host pattern matches the name you type, not the expanded HostName — so ssh web-1 through an alias entry skips the wildcard entirely, and without a ProxyCommand the tailnet name won't even resolve. If you use aliases, either put the ProxyCommand in the alias entry itself (Option 2) or use Match final, which re-matches after HostName expansion and covers typed names and aliases:
~/.ssh/config — alias-proof wildcard
1Match final host="*.work.ts.net"2    ProxyCommand tailmux connect %h %p

Option 2 — per-host entries, and GUI tools like SSH Config Editor#

GUI SSH managers (SSH Config Editor, Termius, an IDE's SSH targets) work per host entry. Add the ProxyCommand to the host, right next to the user and key you already configured there:

~/.ssh/config — a single host entry
1Host web-12    HostName web-1.work.ts.net3    User admin4    IdentityFile ~/.ssh/id_ed255195    ProxyCommand /usr/local/bin/tailmux connect %h %p
GUI apps don't inherit your shell's PATH, so use the absolute path to the binary — which tailmux prints it (typically /usr/local/bin/tailmux, or /opt/homebrew/bin/tailmux with Homebrew on Apple Silicon).

Aliases work: SSH expands HostName before substituting %h, so the ProxyCommand sees the real tailnet name (web-1.work.ts.net) and suffix routing applies even though you typed ssh web-1 — or clicked the host in the app. Everything else in the entry — identity files, agents, port forwards — keeps working as usual, because TailMux only carries the TCP stream.

Fixed local port with tailmux tunnel#

The CLI equivalent of an app tunnel — for scripts, for Linux, or when you want the listener only for the length of a session:

zsh — tailmux
$tailmux tunnel --listen 127.0.0.1:2222 host.work.ts.net:22$ssh -p 2222 admin@127.0.0.1

The profile is chosen from the target's suffix; pass --profile <name> to force one (required for raw-IP targets). The listener stays open for many connections until you stop it — unlike tailmux connect, which streams exactly one.

Shells vs services#

You wantUse
An interactive shell or a one-off remote commandtailmux ssh <host>
Plain ssh/scp/git — or a GUI SSH tool — to always workProxyCommand in ~/.ssh/config wildcard or per-host
A service (Postgres, RDP, SMB, port 22 for a GUI tool) on a fixed 127.0.0.1:<port>, kept alive for youa tunnel in the app (Open → Tunnels)
The same fixed port from a script or on Linuxtailmux tunnel
A single raw TCP stream on stdin/stdout (ProxyCommand-style hooks)tailmux connect <host> <port>

Rule of thumb: tailmux ssh gives you a shell; a tunnel gives a client a port.

Notes#

  • The owning profile must be logged in (tailmux profile login <name> or the login button in the app) before any dial succeeds.
  • Routing is by hostname suffix with no cross-profile fallback: if the owning profile can't reach the host, the connection fails rather than retrying on another tailnet.
  • tailmux run ssh … does not work: tailmux run sets HTTP_PROXY-style variables, which OpenSSH ignores. That's why the SSH paths above exist.
  • Host-key verification is never bypassed or reimplemented — TailMux only carries the TCP stream.