An AI agent can run every command in this README, but a few things require you, because macOS and Apple security won't let any script do them. Read this first.
You need installed first:
- Xcode (iOS app)
- Node.js 18+ & npm
- tmux
- Tailscale on BOTH your Mac and your phone
- (recommended) pm2
Steps only a human can do (your AI agent will pause and ask for these):
- Set up Tailscale on both devices and note your Mac's Tailscale IP.
- Build the iOS app in Xcode with your own Apple Team ID to a physical iPhone/iPad, and enter your Tailscale IP in the app before it will connect.
- Edit
PROJECTS_DIRnear the top ofserver.jsto point at where you keep your projects.⚠️ This project is BETA / rough — expect to read the code and adjust. Do not expose the server to the public internet.
A remote tmux terminal for your iPhone and iPad — reach your Mac's shell, and the Claude CLI, from anywhere on your Tailscale network.
Part of vibekit — a showcase of small tools and Claude Code skills that improve coding with AI. By Brian Harms / Ritual Industries.
tether is a two-part system: a small Node server that runs on your Mac and bridges WebSocket clients to live tmux sessions via a real PTY, plus a native SwiftUI iOS app that attaches to those sessions over your private Tailscale network. The result is a fast, real terminal in your pocket — you can attach to a running session, type commands, scroll back through history, spin up new sessions inside a project folder, and even drive the Claude CLI from your phone.
It is purpose-built around an AI-coding workflow: new sessions can auto-launch claude --dangerously-skip-permissions, there's a structured "chat mode" that runs claude -p --output-format stream-json and renders thinking / tool-use / responses as chat bubbles, and the server auto-detects dev-server ports printed in a session's output so you can preview your work from the phone.
Maturity: BETA / rough. This is a personal tool that has been cleaned up and made public, not a polished product. It hard-codes a few conventions (notably the projects directory and the server ports), the iOS app requires you to fill in your own Tailscale IP and Apple Team ID before it will build, and the TLS layer uses self-signed certs that clients are configured to trust unconditionally. It works well on a trusted private network (Tailscale). Do not expose it to the public internet. Expect to read the code and adjust paths to fit your setup.
- Real PTY-backed tmux sessions — attach, detach, create, rename, and kill
tmuxsessions from the phone; output streams over WebSocket with 16ms batching to avoid line tearing. - Dimension syncing — the server resizes the tmux pane to match the phone's terminal and forces a redraw, so TUI apps render at the right width.
- Project-aware session creation — lists folders in your projects directory and can create a new tmux session
cwd'd into a chosen project; optionally auto-launches the Claude CLI. - Structured Claude chat mode — runs
claude -p --output-format stream-json --verbose, streamscontent_block_*events, and renders thinking / tool-use / text as chat bubbles with resume support. A second, terminal-scrapingChatParser(configurable viachat-patterns.json) is also included. - Live preview — auto-detects dev-server ports from session output (via
lsof+ log-pattern matching) and reverse-proxies them, or serves previewable files (HTML, images, PDF, Markdown, etc.) straight from a session's working directory. - Photo upload — POST a photo from the phone; the server stores it under
~/Desktop/Uploadsand downsizes anything wider than 2048px withsharp. - Voice-to-terminal dictation — the iOS app uses on-device speech recognition to dictate commands.
- Token auth — if
TETHER_AUTH_TOKENis set, every WebSocket connection must authenticate with it as its first message (5s timeout) before any command is accepted. The iOS app stores the token in the Keychain. - HTTP + HTTPS / WS + WSS — both are served; self-signed certs are generated automatically on first run.
- Resilient client — exponential-backoff reconnect, ping/pong keepalive, auto re-attach to your last session, and human-readable connection-error classification (Tailscale down vs. server not running vs. TLS error).
- PWA fallback —
public/index.htmlis an xterm.js + service-worker web client you can open in a browser if you don't want to build the native app. - iOS terminal UX — SwiftTerm-backed terminal, theme picker, adjustable font/size/line-spacing, haptics, and an iPad split layout.
On the Mac (server):
- macOS (the server shells out to
tmux,lsof, andopenssl; thenode-ptypostinstall step references adarwin-arm64prebuilt helper, so Apple Silicon is the tested path). - Node.js 18+ and npm.
- tmux — the server attaches to and manages tmux sessions.
- Tailscale — installed and logged in; this is how the phone reaches the Mac.
- Claude CLI (
claude) — required for the auto-launch and structured chat-mode features. Install per Anthropic's instructions. - pm2 (optional but recommended) — to keep the server running in the background. The iOS error messages assume you run it under pm2.
openssl— used to generate the self-signed TLS certs (present by default on macOS).
On the phone (client):
- iPhone or iPad on iOS 17.0+.
- Tailscale app, installed and on the same tailnet as the Mac.
To build the iOS app:
- Xcode 16+ on a Mac.
- An Apple Developer Team ID — the project ships with
DEVELOPMENT_TEAMblanked; you must set your own to sign and run on a device. - XcodeGen (recommended) — the project is defined in
tether-ios/project.yml. - Swift package SwiftTerm (resolved automatically by Xcode/XcodeGen).
- Microphone + Speech-Recognition permissions are requested at runtime for dictation; Photo Library and Local Network permissions are requested for upload and connectivity.
git clone https://github.com/brianharms/tether.git
cd tether
npm installCreate your auth token file. .env is gitignored — never commit it.
# generate a strong token and write it to .env
printf 'TETHER_AUTH_TOKEN=%s\n' "$(openssl rand -hex 32)" > .envIf you skip this, the server runs with auth disabled (any client on the network can connect). Setting a token is strongly recommended.
Start the server:
npm startOn first run it generates self-signed TLS certs into certs/ (also gitignored). You'll see:
tether listening on http://0.0.0.0:7778 and https://0.0.0.0:7779
auth: enabled
The server expects your projects to live in ~/Desktop/claude projects/ (this path is currently hard-coded in server.js as PROJECTS_DIR; edit that line if your projects live elsewhere). Uploaded photos go to ~/Desktop/Uploads/.
To keep it running across reboots, use pm2:
pm2 start server.js --name tether
pm2 savetailscale ip -4 # e.g. 100.x.y.zOpen tether-ios/ and configure two things before building:
a. Your Tailscale IP. The placeholder YOUR_TAILSCALE_IP lives in tether-ios/tether/SettingsManager.swift:
static let defaultServerURL: String = "ws://YOUR_TAILSCALE_IP:7778"Replace it with your Mac's Tailscale IP (or leave it and set the server URL in the app's Settings screen at runtime). For HTTPS/WSS use wss://YOUR_TAILSCALE_IP:7779.
b. Your Apple Team ID. In tether-ios/project.yml, DEVELOPMENT_TEAM is blank:
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.tether.app
DEVELOPMENT_TEAM: "" # <- set your 10-char Apple Team ID
CODE_SIGN_STYLE: Automatic
CODE_SIGN_IDENTITY: "Apple Development"Then generate the Xcode project and build:
cd tether-ios
xcodegen generate # produces tether.xcodeproj from project.yml
open tether.xcodeprojSelect your device, build, and run. In the app's Settings, paste the same TETHER_AUTH_TOKEN you put in the server's .env (it's stored in the Keychain).
Simulator note: when built for the simulator, the app defaults its server URL to
ws://localhost:7778and loads mock chat data. For real use, build to a device with Tailscale running.
Instead of the native app, open https://YOUR_TAILSCALE_IP:7779/ (or http://YOUR_TAILSCALE_IP:7778/) in mobile Safari — public/index.html is an installable PWA terminal. You'll need to accept the self-signed cert.
Day to day:
- On the Mac, make sure tmux is running with the sessions you care about (or create them from the app). Keep the tether server alive under pm2.
- Make sure Tailscale is connected on both the Mac and the phone.
- Open the app. It auto-connects, authenticates with your token, lists sessions, and re-attaches to your last session.
- Attach to a session to get a live terminal. Type, scroll (mouse-wheel scroll is bound to viewport scroll in tmux), pinch font size in Settings.
- New session in a project: open the project picker, choose a folder; tether creates a tmux session
cwd'd there and (for the project flow) launchesclaude --dangerously-skip-permissionsfor you. - Chat mode: switch presentation mode to chat to talk to Claude via structured
stream-jsonand see thinking / tool-use / responses as bubbles. - Preview: if a session is running a dev server, tether detects the port and lets you preview it; otherwise it lists previewable files from the session's working directory.
- Dictate commands with the mic, or upload a photo from your library into
~/Desktop/Uploads.
If you get a connection error, the app tells you the likely cause: "Cannot reach Mac Mini" (Tailscale), "Connection refused" (server/pm2 not running), or "SSL/TLS error" (try toggling ws:// ↔ wss://).
This section is for an AI agent working on the tether codebase.
Repo layout (top level):
server.js— the entire Node server: Express REST routes, twowsWebSocket servers (HTTPwssand HTTPSwssSecure), PTY/tmux lifecycle, structured chat-mode spawning, preview/proxy, photo upload, and tmux global config. This is the primary entry point.chatParser.js—ChatParserclass: a terminal-scraping state machine that turns raw PTY output into structured chat messages. Configurable viachat-patterns.json.chat-patterns.json— regex/marker patterns theChatParsercompiles at load.package.json— deps (express,ws,node-pty,http-proxy-middleware,multer,sharp);npm startruns the server;postinstallchmods the node-ptydarwin-arm64spawn-helper.public/—index.html(xterm.js PWA client),manifest.json,sw.js(service worker).tether-ios/— native app.project.ymlis the XcodeGen source of truth;tether/holds the Swift sources.LICENSE,.gitignore.
Key iOS entry points:
tether-ios/tether/tetherApp.swift— app entry.WebSocketClient.swift— all client networking: message encode/decode, reconnect/backoff, ping/pong, auth handshake, REST helpers, andServerMessagedispatch.SettingsManager.swift— persisted settings; holds theYOUR_TAILSCALE_IPplaceholder and reads the auth token from the Keychain.ContentView.swift— top-level UI (iPhone vs. iPad layouts);TerminalWrapper.swiftwraps SwiftTerm.
Build / run / test:
- Server:
npm installthennpm start(orpm2 start server.js --name tether). Ports are 7778 (HTTP/WS) and 7779 (HTTPS/WSS), bound to0.0.0.0. - iOS:
cd tether-ios && xcodegen generate && open tether.xcodeproj, then build to a device. There is no automated test suite; verify changes by attaching from a device or the simulator (simulator defaults tows://localhost:7778and uses mock chat data).
Invariants — do not break:
- Auth token stays in
.env. ReadTETHER_AUTH_TOKENfrom.env/env only; never hard-code it. The auth gate (first-message{type:"auth"}within 5s,client.authenticatedchecked before broadcasting) must remain in place. The iOS side keeps it in the Keychain, not UserDefaults — preserve the legacy-token migration inSettingsManager. - Certs are gitignored and self-signed. Keep
certs/,*.pem,*.key,.env*in.gitignore.ensureCerts()must keep generating them on first run. The clients intentionally trust self-signed certs for local/Tailscale use — that's by design, not a bug to "fix" by adding strict validation (it would break the workflow), but never widen this to public exposure. - Ports 7778/7779 are reserved. The proxy route and
getListeningPorts()/detectSessionPorts()explicitly exclude 7778/7779 and ports ≤ 1023 — keep those guards when touching preview/proxy code. - Input validation.
isValidSessionName/isValidProjectName(^[a-zA-Z0-9_.-]+$, ≤256 chars) gate everything that reachestmuxand the filesystem. All shell-outs useexecFileSync/spawnwith arg arrays — never interpolate user input into a shell string. - Don't send capture-pane scrollback over the live PTY stream. See the long comment in the
attachhandler: mixing plain capture-pane text with raw PTY escape sequences corrupts the terminal. Let tmux redraw viaattach-session. - Keep the 16ms output batching (
enqueueOutput/flushOutputBuffer) to prevent line tearing, and keep the dimension-sync (resize-pane+refresh-client) on attach/resize. - Placeholders stay parameterized.
YOUR_TAILSCALE_IP(SettingsManager.swift) andDEVELOPMENT_TEAM: ""(project.yml) are deliberate — never commit a real IP or Team ID.
MIT © 2026 Brian Harms / Ritual Industries