SSH sessions that survive the network. A native macOS SSH client built with SwiftUI.
Version 0.10.0 · github.com/WhileEndless/tether · Release notes
Tether keeps your remote work alive when Wi‑Fi drops or your Mac sleeps, without installing anything on the server. It also manages your ~/.ssh/config, transfers files with drag and drop, and shows live system information for every host — Linux, macOS, BSD and Windows.
Requires macOS 14.4 (Sonoma) or later, on Apple silicon or Intel. Tether is an early release (0.10.0): expect rough edges, and please report issues.
All screenshots use synthetic demo data (make demo); no real hosts are shown.
![]() |
![]() |
| GPU server with a training run | Docker containers with their published ports |
![]() |
![]() |
| Listening ports, one click to forward | File manager tab with services |
Search across every terminal, the archive and recordings.
Sessions that survive
- Nothing to install on the server. On connect, Tether checks what the server already has:
- tmux ≥ 3.0 → runs it in control mode (
tmux -CC). You never see tmux's UI. Scrolling, selecting and copying are native, and the session keeps running on the server. - GNU screen → a screen session with local scrollback kept intact, shared with any other display of it (never pushed out).
- Neither → a plain login shell, with automatic reconnects (a new shell in the same tab) and a local transcript. Such a server says so once — and how to install tmux there.
- tmux ≥ 3.0 → runs it in control mode (
- Clear states. Online, Waiting (connection lost: history stays visible, what you type waits and is sent when the session is back — asked about first when another program is in front by then) and Dead (the remote session is confirmed gone: read-only and archived). A session detached on the server (
tmux detach, screen's detach key) waits for Reconnect instead of ending. A lost network is detected immediately, and a stalled server within seconds. Tabs reattach when the network returns, and also after the app is quit or crashes. - Background sessions. Close a tab and keep its session running in the background. Sessions a closed tab left running on a server are listed under the host by the tab's name, ready to open again with a click — so are your own tmux sessions. Tabs whose session ended on the server while they were away close by themselves.
- See what runs in them without attaching. Each server session shows the command and folder of its active window (the tooltip and the host panel list every window). Coding agents are recognised: Claude Code, Codex and opencode show the name of their session — the one you gave it or the one the agent chose — on server sessions and on open tabs, and whether Claude Code is working. Tabs you have not renamed take the agent session's name. Names come from the agents' own files on the server (Codex and opencode need
sqlite3there unless you named the Codex thread yourself; otherwise the terminal title is used). The lists refresh on their own while a tab to the host is open; the sidebar's Refresh button (↻) reloads them and the SSH config at once. - Save a session with ⌘S (File ▸ Save Session…): the whole session — everything it has shown, including output no longer in memory (read from its recording) — what the terminal holds now, or just the last command and its output (say the
lsyou just ran), as plain text or as the raw output with colors. It opens in your Downloads folder; what it starts with and where are set in Settings ▸ Terminal ▸ Saving a Session.
Passwords, Touch ID and sudo
- Saving passwords is optional: tick Remember in Keychain when signing in (works for every jump host and for SSH key passphrases).
- Saved passwords stay in the macOS login keychain. Optionally Touch ID (or your Mac's password) is asked before one is used, then not again for a time you choose; locking the screen always asks again.
- A saved password only answers its own host: never a server's own prompt text naming another host, never a jump host asking for the target's password.
- When a terminal asks for a password —
sudo,su,sshto another host, a key passphrase — a bar offers to fill a saved one (⌥⌘P) or to type one in a secure field and save it. Passwords are kept per user: sudo's for the user who runs it, su's for the account it switches to (also on macOS, where both just ask "Password:"). Only sudo is filled automatically, and only while sudo itself is waiting; a password is never typed into a shell (where it would land in the shell history and the recording). Secure Keyboard Entry is on while a terminal waits for a password. - A saved password that stops working (changed on the server) is not sent again: you are asked for the current one, which replaces it. Saved passwords can also be replaced or removed in Settings ▸ Passwords.
- You sign in to a server once per run: a typed password also answers that connection's later sign-ins (same host, port, user and jump host) until Tether quits — encrypted by a Secure Enclave key that exists only while Tether runs, never written anywhere, never used for sudo or one-time codes, and switchable off (even then, that connection's other sign-ins take it for a minute, so many tabs ask once). A saved password asks for Touch ID when it is read from the keychain, then signs in again like a typed one — also after the Mac slept; a password typed for a sign-in through a shared connection is saved only once that sign-in worked, and a one-time code asked after it never replaces it. Details in docs/connections-and-passwords.md.
Security
- App Lock (off by default): Touch ID or the Mac password, or a separate Tether password. Every window is covered until Tether is unlocked; sessions keep running meanwhile. Coming back to Tether asks for Touch ID right away. Optionally locks when the Mac sleeps or its screen locks; ⌃⌘L or the toolbar's lock button locks at once.
- Encrypted recordings (off by default, a simple on/off switch): AES-256-GCM, hardware accelerated, with the key kept behind your Tether password (set when you turn it on, typed twice). Turning it on encrypts every recording, existing and compressed ones too; turning it off decrypts them again. While Tether is locked, output and command history are still kept: they are sealed to the key's public half, and only unlocking opens them. Search, transcripts and scrolling back work as usual. Forgot the password? Reset (after typing RESET and the Mac's own check) removes it and the recordings nobody can open any more, and keeps everything else.
- Security settings are also kept in the keychain, so other programs cannot quietly switch them off; weakening one asks for Touch ID or the Mac password.
Windows servers
- Windows 10/11 and Windows Server with OpenSSH (cmd or PowerShell as the default shell) are detected automatically. Nothing is installed.
- PowerShell terminals, the file manager (drives, upload/download, rename, move, copy, delete), transfers, and the host panel: system, processes, services, network, listening ports, connections, event logs, NVIDIA GPUs and containers.
- Windows has no tmux or screen, so a shell there ends with its connection (its output stays in the recording).
Terminal
- xterm-256color and true color, mouse reporting, alternate screen, Option as Meta (off by default, so Option types your layout's characters such as @), and key mappings. vim, htop, fzf and emacs work as expected.
- Uses this Mac's Terminal.app profile (colors, font, translucency) by default.
- Terminal.app navigation: ⌘↑/⌘↓ jump to the command above or below what is on screen (into the recording too), ⇧⌘↑/⇧⌘↓ select whole commands, and PageUp/PageDown/Home/End scroll.
- Tab names follow the running command (or the current directory), and you can rename any tab.
- Local terminals on this Mac. These are persistent too when tmux is installed locally.
Tabs and windows
- Each window has its own tabs. Switching only swaps the terminal (instant); the sidebar and panels stay in place.
- double-click to rename
- live status dots
- hold ⌘ to see tab numbers (⌘1…⌘9; ⌃Tab and ⇧⌘[ ] to cycle)
- drag a tab out into its own window, or onto another window's tab bar
- middle-click to close; the mouse wheel scrolls the tab bar, and ⌄ lists every tab
- ⌘P quick switcher and Merge All Windows. Only the visible tab is drawn, so many tabs stay cheap.
- ⌘T opens a new tab to the same host in the folder the current tab's shell is in, like Terminal.app — locally and on servers. New tabs open next to the current one. Both can be changed in Settings ▸ General.
- Notifications from programs. When Claude Code finishes or has a question, when a long job prints
\e]9;done\a, or when the bell rings in a tab you are not looking at, the tab gets a bell and macOS shows a notification; clicking it brings the tab to the front. While Tether is locked, or its recordings are encrypted, a notification says only that a program wants your attention (no text from the terminal), and locking removes those already shown. The common sequences are understood: OSC 9 (iTerm2 style), OSC 777 (rxvt, Ghostty), OSC 99 (kitty) and the bell — so any of Claude Code's notification channels works. - Close a tab to keep its session in the background, or terminate it. You can also terminate every session on a host at once.
- Disconnect & Move to Background drops the connection but leaves tmux/screen running on the server. The session shows as Suspended and reattaches when you open it again.
- File managers open as tabs too, next to terminals. Each keeps its directory and selection, even in the background.
Hosts and connections
- Comment-preserving editor for
~/.ssh/config(andIncluded files), with backups before each save. - Jump host chains (ProxyJump), each hop with its own saved password.
- Identify by name (HostKeyAlias), so an address reused on another network is not confused with your host.
- Direct connections (user, host, port, jump host, password or key) without saving anything.
- One authenticated connection per host (ControlMaster), shared by terminals, transfers, monitoring and port forwards; one sign-in however many of them start at once. Hosts behind the same jump host share its connection.
- Connections survive short outages, Wi-Fi changes, the screen locking and sleep: patient keep-alives keep firewalls from forgetting idle ones but end a connection only after 10 minutes without any answer; servers are not sent data while the Mac sleeps; after waking each connection is checked over its own link without running anything on the server, and only a dead one is replaced. With connection sharing on, your ssh_config's ConnectTimeout and keep-alive interval are kept. See docs/connections-and-passwords.md.
- Passwords stay in the keychain and reach
sshvia a private askpass bridge. - SSH keys: Set Up SSH Key… on a host creates a key (Ed25519, or RSA 4096 for older servers), with or without a passphrase (typed twice), adds it to the server's
authorized_keys, checks that the key alone logs in, and sets it in your SSH config. An existing key can be used instead. Existing files are never replaced. It is offered only where key login doesn't already work. - Keys with a passphrase: when ssh asks for it, Tether answers with the passphrase it keeps in your keychain (after Touch ID, if you turned that on), or asks you and can remember it — once per key file, however many hosts and tabs use it (see docs/connections-and-passwords.md). Optionally new keys also go to the macOS ssh-agent for Terminal and git. Settings ▸ SSH ▸ Keys lists your keys and adds, changes or removes a passphrase.
- Port forwarding:
- local, remote and SOCKS forwards saved per host, with auto-start
- conflict checks with free-port suggestions
- one-click forwarding of ports the server listens on
- forwards are restored automatically after a reconnect, also for hosts without an open tab, and after a relaunch (those listening only on this Mac by themselves; others when you say so)
Host panel
- A live status bar above every terminal: CPU, memory, disk, load, GPU, latency and active forwards.
- Connection details: server address and port, the address you appear as, sshd ports, and latency with jitter and a quality rating.
- Metrics with sparklines, disks, and GPUs (NVIDIA via
nvidia-smi, including unified-memory systems such as DGX Spark; AMD viarocm-smior sysfs). - Processes with signals, systemd services (start, stop, restart, logs), and Docker/Podman containers with their published ports and whether each is public or local only.
- Kubernetes: pods (status, ready containers, restarts, node, age) and nodes of the cluster that
kubectl, k3s, microk8s orocon the host can reach, filtered by namespace or name, with logs, describe and Delete Pod. - Listening ports:
- every TCP/UDP port the server listens on, with the process and container behind it
- grouped by reach: all interfaces, one interface, or local only
- one-click forwarding to this Mac
- Network:
- per-interface download/upload rates, addresses, MAC and link speed
- real interfaces first, virtual ones folded away
- System, auth and kernel logs, logged-in users, and connections (SSH detected by sshd's real ports).
- Every table exports as CSV (processes, listening ports, logged-in users, TCP connections, services, containers, pods, GPUs, traffic by process); the command history also as JSON.
- Monitoring streams over one channel and only runs while something is visible.
Files
- Remote file browser as a tab, a panel or a full window, with drag and drop in both directions, Quick Look and details.
- Directories you have visited open instantly from a cache and refresh in the background, keeping scroll position and selection.
- Browse Files Here (the terminal's current directory) and Open Terminal Here. In a terminal of this Mac, Browse opens the folder in Finder (or Tether's file manager, Settings ▸ General).
- Transfers show what went where, speed, amount transferred and ETA. Many files (5 or more by default) travel as one packed stream (tar on both ends, nothing written to disk in between), compressed as it flows with zstd, lz4 or gzip — the fastest both ends have — or xz or bzip2; others are copied several at a time. All of it is configurable, compression can be turned off, and every transfer of a server shares its one sign-in.
- Drop or paste files into a terminal to upload them — Finder files, the screenshot thumbnail, or an image on the clipboard (⌃⇧⌘4 screenshots are saved as PNG first): they go to the folder the terminal is in (Settings ▸ Transfers: or
/tmp, or ask each time; hold ⌥ to be asked once), then the remote path(s) are typed at the prompt. Existing files are never overwritten — a free name such asreport-1.pdfis chosen instead. In local terminals (this Mac) nothing is copied: the file's own path is typed (a pasted image is saved as a PNG first, and kept for a week). Paths dropped or pasted reach a full-screen program too when it takes pastes as pastes (vim, an agent's prompt); otherwise they are copied, and the terminal says so. - Copy and paste files anywhere: ⌘C in the file manager, then ⌘V in another folder (copied on the server itself), in another server's file manager or terminal (through this Mac — the temporary copy is removed afterwards; into a terminal they go where Finder files would), in a terminal of the same server (the paths are typed), in a terminal on this Mac (saved where its files go — the terminal's folder, else Downloads; see Settings ▸ Transfers — and their paths typed), or in Finder. Rows of this Mac's own file manager are not copied for a terminal here: their paths are typed. Dragging rows does the same. Files copied in Finder are uploaded. Folders and files can be mixed; many files travel as one packed stream. Nothing is ever replaced, and the file manager says how a paste went (pasted, renamed, what could not be downloaded) — with a macOS notification when Tether is in the background. Transfers (⌥⌘L) is in every file manager window's toolbar too. Bulk move, copy, permissions, compress, rename and delete.
- Search: the search field (⌘F) filters the folder as you type; ↩ searches the folders below it on the server — names containing the text, a pattern such as
*.log, or files containing the text — with onlyfindandgrep(PowerShell on Windows). Results show where they are; every file action works on them, and Show in Enclosing Folder goes there.
Search
- Search every open terminal, the archive and old recordings, with plain text or regex, case sensitivity and whole words.
- Filter by host, session, status and time (presets such as Last 15 minutes or an exact from/to range).
- Results show highlighted context. Double-click a result to open the terminal (or transcript) at that line; the terminal keeps the search, so ⌘G / ⇧⌘G step through the other matches there.
- Copy lines, matches or ranges. Export results as text, CSV or JSON, or export full transcripts.
- Long sessions are covered too: output that was dropped from memory is searched in the recording, without duplicates.
- The text is what the terminal showed: full-screen programs (vim, less, htop) leave nothing behind, progress bars and spinners count once in their final state, and programs that redraw everything (Claude Code) appear once, not once per redraw. The same goes for plain-text export and Save Session.
- A progress bar shows which source is being searched, and you can stop at any time. Compressed recordings are skipped unless you include them.
Long sessions and the menu bar
- No limit on how long a session runs: memory keeps a set number of lines, and scrolling further reads earlier output back from disk without a visible break (view and selection stay in place).
- Closing the window keeps Tether and every session running in the menu bar; the icon brings the window back with its tabs.
- After quitting — or a crash — every window comes back with its tabs, selected tab, size, position and full-screen state.
- Dozens of tabs to one host: one sign-in, and terminals are spread over several shared connections (servers allow 10 sessions per connection by default).
- Monitoring pauses while no Tether window is visible, and recordings are written in large batches to spare the SSD.
Local backups.
- Every session's output is recorded locally and can be replayed read-only, at the size the terminal had.
- Long sessions are compressed while they run, in pieces of 16 MB (default); the latest output stays uncompressed, so scrolling back stays fast. Recordings older than 7 days are compressed as a whole (LZFSE, typically 5–10× smaller).
- The 1 GB limit (default) removes the oldest recordings first; a session holding more than half of it loses its own oldest output before any other recording goes.
- Programs cannot erase the history on screen (
clear, Claude Code's redraws) unless you allow it (Settings ▸ Terminal); the recording keeps everything either way. - What is kept where, for how long and what goes first: docs/data-storage.md.
- Only a configurable number of lines stays in memory. Older output is opened from disk with "Open Full History", which loads large recordings in slices.
Anything with ⌘ belongs to the app. Everything else — Ctrl keys, Option (characters, or Meta if you turn it on), Esc, function keys — goes straight to the terminal, so vim, emacs, tmux bindings and fzf behave as usual. The full, searchable list is in Help ▸ Keyboard Shortcuts (⌘/) and in Settings ▸ Keyboard.
| New connection / quick connect (host search) / tab to same host / local terminal | ⌘N / ⌘↩ / ⌘T / ⌥⌘T |
| Close tab / close window | ⌘W / ⇧⌘W |
| Next / previous tab | ⇧⌘] ⇧⌘[ (also ⌃Tab, ⌥⌘→ ⌥⌘←) |
| Tab 1…8 / last tab · go to any session | ⌘1…⌘8 / ⌘9 · ⌘P |
| Move tab to background / disconnect to background | ⇧⌘B / ⌥⇧⌘B |
| Terminate session | ⌥⇧⌘W |
| Save the session (or the last command) to a file | ⌘S |
| Fill a saved password (sudo, su, ssh prompt) | ⌥⌘P |
| Lock Tether (with App Lock on) | ⌃⌘L |
| Jump between commands / select a command's output | ⌘↑ ⌘↓ / ⇧⌘↑ ⇧⌘↓ |
| Find in terminal / search all sessions | ⌘F (⌘G, ⇧⌘G, ⇧⌘E) / ⇧⌘F |
| Browse files here / file manager window / transfers | ⌘E / ⌥⌘O / ⌥⌘L |
| Sidebar / host panel | ⌃⌘S / ⌥⌘I |
| Clear scrollback · font size | ⌘K · ⌘+ ⌘- ⌘0 |
Menu shortcuts can be changed in System Settings ▸ Keyboard ▸ Keyboard Shortcuts ▸ App Shortcuts. Terminal key mappings (for example ⌘← → Ctrl-A) are set in Settings ▸ Keyboard.
- macOS 14.4 (Sonoma) or later. Release downloads run on Apple silicon and Intel Macs. On macOS 26 Tether uses Liquid Glass; earlier systems get the standard look.
- To build: Xcode 26 or its Command Line Tools (Swift 6.2), which is what Tether is built and tested with. Xcode 16 (Swift 6.0, needed on macOS 14) is meant to work too, without Liquid Glass, but is not tested. Check with
swift --version. - On servers: nothing. tmux or GNU screen make sessions persistent if they are there;
tar(present almost everywhere) speeds up transfers of many files.
- Download
Tether-<version>.zipfrom Releases and unzip it. - Move
Tether.appto/Applications. - The app is not notarized yet, so macOS blocks the first launch. Allow it in System Settings ▸ Privacy & Security ▸ Open Anyway (on macOS 14 you can also right-click Tether.app ▸ Open ▸ Open). From a terminal you can instead run:
xattr -dr com.apple.quarantine /Applications/Tether.app
git clone https://github.com/WhileEndless/tether.git
cd tether
make install # builds a release for this Mac and copies it to /Applicationsmake install INSTALL_DIR="$HOME/Applications" installs for your user only. make zip builds the release download (Apple silicon and Intel in one app).
Or make run to build and open it from dist/ without installing. A build made on your own Mac is not quarantined, so it opens without the warning.
Tether updates itself (Settings ▸ General ▸ Updates, on by default): at launch and once a day it asks GitHub whether a newer release exists, downloads that release's source into a temporary folder, builds it there in the background at low priority, and installs it with make install over the copy that runs when you quit Tether — or at once with Restart to Update (also in the Help menu and the sidebar). Help ▸ Check for Updates… asks right away and opens the Software Update window: the new version's release notes, the progress of the download and of the build (Cancel stops it), and Restart to Update once it is ready. After an update, Tether shows once what changed. Building needs Apple's command line tools (xcode-select --install); without them, or with Download and install updates off, Tether only says a new version exists and links to its release page. Turn the check off and nothing is asked at all.
By hand: quit Tether, then install the new version over the old one (git pull && make install from source). Either way settings, hosts, sessions and recordings are kept, and sessions that were open reattach on the next launch.
Tether is not signed with a developer certificate. With the default ad-hoc signature every build is a new app to macOS: the first time a new version uses a saved password (or the recording key), macOS asks whether Tether may use it (choose Always Allow), and folder permissions (Documents, Downloads …) are asked for again. Building from source, run make signing-identity once: it creates a local, self-signed signing certificate in your login keychain, every later build is signed with it, and macOS keeps those permissions across updates. Tether's own security settings are read without asking either way.
Programs you run in Tether's terminals (a build, git, a coding agent) reach macOS as Tether, as they would as Terminal.app: when one of them opens a file in Documents, macOS asks whether Tether may. A terminal (or tmux server) started by an older build still belongs to that build, so after updating, reopen local terminals once.
make uninstall # or drag /Applications/Tether.app to the TrashYour data stays until you remove it:
| What | Where |
|---|---|
| Settings, host groups, session list, recordings | ~/Library/Application Support/Tether |
| Saved passwords | Login keychain (search for "Tether" in Keychain Access) |
| Your SSH config | ~/.ssh/config — Tether edits it only when you save a host, and keeps a backup |
Tether sessions that are still running on servers can be ended from the sidebar (Terminate) before uninstalling, or later with tmux kill-session -t tether-… on the server.
- ⌘N to connect: pick a host from your ssh config or type
user@host:port, optionally with a jump host. - Hosts from
~/.ssh/configappear in the sidebar. Click one to see its status; double-click to open a terminal. - ⌥⌘I shows the host panel (system, processes, services, containers, ports, logs). ⌘E opens a file manager in the terminal's current folder.
- ⌘/ lists every keyboard shortcut.
make # build the app → dist/Tether.app
make help # list all targets
make build # compile (debug)
make test # unit and local integration tests — no server needed
make app # release build → dist/Tether.app
make debug # debug build → dist/Tether.app
make zip # dist/Tether-<version>.zip for a release
make demo # open the app with synthetic data (SHOTS=1 saves screenshots)
make check # privacy scan + release notes + tests
make clean # remove build productsmake only wraps swift build, swift test and scripts/build-app.sh; you can run those directly. The app is ad-hoc signed. To distribute it without the first-launch warning, sign and notarize it with your own Developer ID.
Tether is a Swift package split into small modules:
| Module | Responsibility |
|---|---|
TetherCore |
Models, settings, paths, keychain |
SSHConfigKit |
Round-trip-safe ssh_config parser and editor |
SSHTransport |
ssh/scp arguments, ControlMaster, askpass bridge, remote command runner |
TmuxControl |
tmux control mode protocol parser and command encoder |
SessionKit |
Terminal sessions, remote bootstrap, persistence and reconnect state machine |
TerminalText |
What a terminal shows without a view: the scrollback rule and the text of recordings (for search and export) |
TerminalUI |
SwiftTerm-based terminal view, themes, key mappings |
RecordingKit |
Session transcripts in compressed parts, encryption, storage quota |
FileTransferKit |
Remote file listing and scp transfers |
MonitorKit |
System, process, log and connection probes |
KeySetupKit |
Key generation and installation |
SearchKit |
Search over live terminals and recordings, export |
TetherApp |
The SwiftUI application |
TetherAskPass |
Tiny SSH_ASKPASS helper that forwards prompts to the app |
Tether runs the system /usr/bin/ssh, so everything in your ssh config works: agents, FIDO keys, ProxyCommand, certificates, Include.
When a terminal connects, Tether runs a short POSIX sh script on the server. It chooses tmux, screen or a plain shell, reports the choice with a private escape sequence, and execs into it. On reconnect, Tether asks only to re-attach. If the session no longer exists, the tab is marked dead and archived instead of quietly starting a new shell.
Tether talks only to the hosts you connect to. Settings, host groups, recordings and the command history stay in ~/Library/Application Support/Tether (recordings and the history can be encrypted, see Security). Passwords stay in your login keychain.
Tether installs nothing on your hosts. Every script it runs there — when, what it reads, what it changes and how it ends — is listed in docs/remote-commands.md and in Settings ▸ Monitoring ▸ What Runs on Hosts. What Tether keeps on this Mac, where, and for how long is described in docs/data-storage.md.
Nothing else is sent anywhere: no analytics, no telemetry. At launch Tether asks GitHub once whether a newer release exists (Settings ▸ General can turn this off); if GitHub cannot be reached, nothing happens.
Issues and pull requests are welcome. See CONTRIBUTING.md and the project rules in CLAUDE.md (privacy, English only, one release-notes file per version).
Tether is released under the MIT License. It uses SwiftTerm (MIT); see THIRD_PARTY_NOTICES.md.
Tether is an independent project and is not affiliated with Apple, tmux or GNU screen.





