Skip to content

brianharms/tether

Repository files navigation

tether

⚠️ Before you start — what YOU (the human) must do

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_DIR near the top of server.js to 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.


What it does

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.

Features

  • Real PTY-backed tmux sessions — attach, detach, create, rename, and kill tmux sessions 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, streams content_block_* events, and renders thinking / tool-use / text as chat bubbles with resume support. A second, terminal-scraping ChatParser (configurable via chat-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/Uploads and downsizes anything wider than 2048px with sharp.
  • Voice-to-terminal dictation — the iOS app uses on-device speech recognition to dictate commands.
  • Token auth — if TETHER_AUTH_TOKEN is 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 fallbackpublic/index.html is 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.

Requirements

On the Mac (server):

  • macOS (the server shells out to tmux, lsof, and openssl; the node-pty postinstall step references a darwin-arm64 prebuilt 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_TEAM blanked; 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.

Setup / Install

1. Server (on your Mac)

git clone https://github.com/brianharms/tether.git
cd tether
npm install

Create 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)" > .env

If 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 start

On 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 save

2. Find your Mac's Tailscale IP

tailscale ip -4   # e.g. 100.x.y.z

3. iOS app

Open 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.xcodeproj

Select 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:7778 and loads mock chat data. For real use, build to a device with Tailscale running.

4. (Optional) Web client

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.

Usage

Day to day:

  1. 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.
  2. Make sure Tailscale is connected on both the Mac and the phone.
  3. Open the app. It auto-connects, authenticates with your token, lists sessions, and re-attaches to your last session.
  4. 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.
  5. 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) launches claude --dangerously-skip-permissions for you.
  6. Chat mode: switch presentation mode to chat to talk to Claude via structured stream-json and see thinking / tool-use / responses as bubbles.
  7. 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.
  8. 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://).

For AI coding agents

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, two ws WebSocket servers (HTTP wss and HTTPS wssSecure), PTY/tmux lifecycle, structured chat-mode spawning, preview/proxy, photo upload, and tmux global config. This is the primary entry point.
  • chatParser.jsChatParser class: a terminal-scraping state machine that turns raw PTY output into structured chat messages. Configurable via chat-patterns.json.
  • chat-patterns.json — regex/marker patterns the ChatParser compiles at load.
  • package.json — deps (express, ws, node-pty, http-proxy-middleware, multer, sharp); npm start runs the server; postinstall chmods the node-pty darwin-arm64 spawn-helper.
  • public/index.html (xterm.js PWA client), manifest.json, sw.js (service worker).
  • tether-ios/ — native app. project.yml is 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, and ServerMessage dispatch.
  • SettingsManager.swift — persisted settings; holds the YOUR_TAILSCALE_IP placeholder and reads the auth token from the Keychain.
  • ContentView.swift — top-level UI (iPhone vs. iPad layouts); TerminalWrapper.swift wraps SwiftTerm.

Build / run / test:

  • Server: npm install then npm start (or pm2 start server.js --name tether). Ports are 7778 (HTTP/WS) and 7779 (HTTPS/WSS), bound to 0.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 to ws://localhost:7778 and uses mock chat data).

Invariants — do not break:

  • Auth token stays in .env. Read TETHER_AUTH_TOKEN from .env/env only; never hard-code it. The auth gate (first-message {type:"auth"} within 5s, client.authenticated checked before broadcasting) must remain in place. The iOS side keeps it in the Keychain, not UserDefaults — preserve the legacy-token migration in SettingsManager.
  • 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 reaches tmux and the filesystem. All shell-outs use execFileSync/spawn with 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 attach handler: mixing plain capture-pane text with raw PTY escape sequences corrupts the terminal. Let tmux redraw via attach-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) and DEVELOPMENT_TEAM: "" (project.yml) are deliberate — never commit a real IP or Team ID.

License

MIT © 2026 Brian Harms / Ritual Industries

About

iOS terminal app that connects to a Mac over Tailscale, with persistent sessions you can detach and resume. Beta.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages