Hosts, machines and devices

On this page

The host owns the threads; a window, a browser or a phone is a client of it (The window and the host). This page covers running the host without a window, reaching it from elsewhere, working on other machines, the web client and the mobile app. Updates and machines has the short version.

Run the host as a system service#

The host can run as a service of the machine, so threads, terminals and paired devices keep working with no Tau window open and after a restart of the machine. Install it in Settings → Connections → Background, or from a terminal:

Task Command
Install and start (again: repair) tau service install
Where it stands, and its log tau service status
Restart it tau service restart
Stop it and remove it from login tau service uninstall

An invisible display on Linux#

On Linux, tau service install --display gives the service an invisible display: tau-xvfb.service runs Xvfb (no TCP, a cookie in <userData>/display/Xauthority) whenever the host runs, and the host hands its DISPLAY to terminals, agents' shell commands and project scripts, so GUI apps and headed browsers run where nobody sees them. When a thread needs the preview and no Tau window is attached, the host starts tau-window.service, a Tau window on that display, and stops it after 10 minutes without use (it costs about 200–300 MB). The .deb brings Xvfb along; elsewhere install your distribution's xvfb package first. tau service install --no-display removes the display, and Settings → Connections → Background shows, adds and removes it too.

Where the kernel restricts user namespaces (Ubuntu 24.04 and later), the window needs an AppArmor profile that allows them for Tau's binary; it never starts with --no-sandbox. The .deb installs that profile. For a copy unpacked anywhere else, Tau writes one for its own path (/etc/apparmor.d/tau-<hash>: that path and userns, like the .deb's) and loads it, with one password prompt: adding the display in Settings (or the Add AppArmor Profile button the service status shows) asks in the system's dialog through pkexec, tau service install --display asks through sudo in the terminal. Tau reads its own AppArmor label and /etc/apparmor.d to know whether the profile is there. The profile belongs to the path, so updates in place need nothing more; a copy that moves needs a new one. Without a polkit agent (an SSH login), the button says so and points to the terminal. A profile for a folder you can write to lets whatever binary you put at that path use user namespaces, which every program may on distributions without the restriction.

This holds on a machine with a desktop session too: a Wayland desktop hands WAYLAND_DISPLAY and XDG_SESSION_TYPE to every user service, so the units unset them (with WAYLAND_SOCKET), the window starts with --ozone-platform=x11, and the host drops them before it starts any shell. Nothing Tau starts lands on the real screen. A unit an older Tau wrote shows as stale in the status; install again to replace it. macOS and Windows refuse the option: neither has a display that nobody sees.

How a service and a window share the host#

The service runs the app's own binary on the app's own userData, so a Tau window adopts the host it finds in host.json like any other and never starts a second one; quitting the window leaves it running. Installing from a window moves that window's threads into the service host, on the same port. An instance with its own TAU_USER_DATA gets a service of its own (a suffix on the names). Network access (Settings → Connections) lives in <userData>/network.json, so the service host opens the same Local network, Tailscale and proxy listeners, on the same ports, once it has taken over. Uninstalling leaves threads and settings where they are.

After an update the window finds the service on the old version and restarts it once; if the unit points at another copy of Tau, it rewrites the unit for this one and restarts it once more. A service that still answers with another version is stopped, and the window runs its own host until the next start. The unit restarts a host only after a crash (KeepAlive.SuccessfulExit false, Restart=on-failure), so a window stopping it never starts a loop.

Keep this machine awake while turns run, beside it, holds off sleep while any thread works: caffeinate on macOS, systemd-inhibit on Linux, SetThreadExecutionState on Windows. It applies to a host in its own process, service or not.

Reach the host over a socket#

The renderer talks to the host through one versioned protocol (ADR 0010); Electron IPC is one transport of it. Start a host that also listens on a socket with TAU_HOST_LISTEN=127.0.0.1:7788 npm start, and point a client at it by opening the workbench with ?host=ws://127.0.0.1:7788&token=<token>, where the token is the line in ~/.tau/host-token (created on the first listen, 0o600). A wrong token closes the connection. Encryption is TLS's job (TAU_HOST_TLS=1, below) or an SSH tunnel's.

npm run smoke:remote-host proves the plumbing without a window: it starts src/main/headless.ts in a scratch repository, says hello, fetches the bootstrap, sends a prompt, disconnects, reconnects with lastSeq and checks that the pushes missed in between are replayed. It runs twice: in plaintext, and over TLS with the printed fingerprint pinned, where a wrong fingerprint and a plaintext socket must be refused.

Run the host on another machine#

The host and the window need not be the same machine. The workspace, Pi, the models and every tool stay on the host; the Electron window is only a client of the protocol above.

On the host machine, start a host without a window:

npm run build
TAU_WORKSPACE=/path/to/project TAU_HOST_LISTEN=127.0.0.1:7788 node dist-electron/main/headless.js

It prints the URL it listens on and the path of its token. Without TLS the socket is unencrypted and repeats that token in every hello, so the host refuses to bind anything but a loopback address; TAU_HOST_INSECURE=1 overrides that for a network you already trust, and the host prints a warning when it does. There are two ways across machines: TLS (next section) or an SSH tunnel. For the tunnel, forward the port from the client:

ssh -N -L 7788:127.0.0.1:7788 you@host-machine

Then copy the host's ~/.tau/host-token to the client machine (or pass it as TAU_HOST_TOKEN) and start Tau as a client:

TAU_HOST_URL=ws://127.0.0.1:7788 npm run start:existing

The main process supervises no host of its own in that mode: it opens the window, which speaks the protocol over the socket, and the kits' code is fetched from the host and served to the renderer from here. What needs this machine (the clipboard, image previews, rebuilding the workbench) is answered in the window process rather than sent to the host. Paths in the workbench (the project's cwd, changed files, a tool's output) are the host's paths, so an action that hands a path to a local tool points at a directory that exists only there. The socket transport says so by leaving the local-files capability out of its hello, which the Electron transport announces.

Without a tunnel: TLS#

A host with TLS may listen on any interface, such as its Tailscale address:

TAU_WORKSPACE=/path/to/project TAU_HOST_LISTEN=100.64.0.7:7788 TAU_HOST_TLS=1 node dist-electron/main/headless.js

On first start it creates a self-signed certificate under its userData (~/.tau/headless/tls/, key 0600) and keeps it across restarts. When the certificate nears its end, the host renews it with the same key. Besides the socket and token lines it prints the certificate's fingerprint and its public key:

tau-host listening on wss://100.64.0.7:7788
tls fingerprint: SHA256 6F:AB:DF:…:10:E9:1E (browsers show it; a renewal changes it)
tls public key: SHA256 9A:38:0C:…:7D:21:B4 (a client pins it as TAU_HOST_PUBLIC_KEY; a renewal keeps it)

TAU_HOST_TLS_CERT and TAU_HOST_TLS_KEY use a certificate of your own instead. On the client, copy the token as above and pin the key:

TAU_HOST_URL=wss://100.64.0.7:7788 TAU_HOST_PUBLIC_KEY=9A:38:0C:…:7D:21:B4 npm run start:existing

TAU_HOST_PUBLIC_KEY also takes the sha256/<base64> form that curl's --pinnedpubkey uses, and so does TAU_HOST_FINGERPRINT. A hex TAU_HOST_FINGERPRINT still pins one certificate, and a renewal breaks that pin. Without either variable the window shows the host's key on first connect and asks whether to trust it; compare it with the line the host printed. A yes saves the key in the client's known-hosts.json. An entry saved before key pins holds a certificate; the first connection it lets in replaces it with that certificate's key. A host whose certificate a CA vouches for needs no pin. If the host ever presents another key (or, for a certificate pin, another certificate), the window refuses it before sending the token, and the status line shows both values. If you replaced the key yourself, update the pin or delete the known-hosts entry. host-protocol.md has the details.

A dropped link (a suspended machine, a restarted tunnel, a phone that slept or changed networks) is expected: the client reconnects with backoff, says hello again with the sequence it last saw and replays what it missed. Heartbeats find a link that died without a close, and coming back to the page or to a network tries again at once. A window's own host keeps its port across a restart of the app while nothing else took it, so a tab opened on it reconnects instead of asking to pair again. A strip above the status line reads Reconnecting to the host… (with "Retry now" while it waits), then Refetching the workbench state… if the host's buffer no longer reaches back far enough. For a host on another machine, a dot in the title bar shows the link and its round trip. Nothing has to be restarted by hand.

The host accepts sockets only from pages it served itself and from clients that are not pages (the native app's sockets send no Origin); a proxy that changes the host name is added with TAU_HOST_ALLOWED_ORIGINS=https://….

Other machines in the same window#

The easier way to work on another machine is to add it to the window you already use. On the other machine, open Settings → Connections, turn on network access and create a pairing link. On this one, open Settings → Machines and paste the link, type the other machine's address (studio.local:7788), or click Find Machines to list the ones that announce themselves on this network (the other machine needs Local network and Announce on) and Add one. The other machine's window asks whether to let this computer in and shows six digits; allow it if this window shows the same six. Tau keeps that machine's key encrypted in the system keychain and never saves it anywhere it cannot.

From a terminal, over SSH. If you can already ssh rex, one command pairs the two machines with no link to copy and no digits to compare:

tau machines add --ssh rex --agents        # --name <name>, --access read-only, --json
tau machines list                          # --json for scripts and agents
tau machines remove rex

Tau must run on both machines, the same version or newer on rex, and rex must accept connections from this computer (Settings → Connections → Network access). tau machines add runs tau machines accept-ssh on rex through your own ssh (your config, agent and known_hosts; it never asks for a password). That makes a pairing link that lives two minutes and hands it back over the SSH session only, never through a file, an argument or an environment variable. This computer asks with it, pinned to rex's key as a pasted link would be, and rex's command line allows that one request with rex's own host token. Anyone who can log in to rex over SSH could read that token anyway. This computer's window keeps rex, as Settings → Machines would. --agents also lets this computer's agents work there, and without a window only the agents keep it. Running the command again only checks the connection. tau machines remove forgets rex here; rex lists this computer until its owner revokes it in Settings → Connections there. agents/pairing-machines.md is the short version for agents.

From then on the rail ends with Other machines: each with a dot for its status (connected with its round trip, connecting, offline since when, refused and why), and its newest threads, including the ones that are running. Clicking a thread opens this window on that machine: its threads, projects, terminals, files and kits are that machine's, and so is everything the agent does. Run on in a new thread's draft moves the draft, text and all, to another machine before it starts. Returning is the same click on a thread of this machine, the machine's name in the title bar, or Back to this computer in the command palette. When the other machine moves to another network, Tau follows its new addresses the next time it reaches it. Settings → Machines can also show the last machine again when Tau starts. Every move loads the window again, which takes a moment the first time a machine's kits are compiled. ADR 0025 explains the design and what is left out: the folder picker stays with the machine the window runs on, and Preview on another machine is a live picture you can click, drawn there by a Tau window of that machine.

Work on another machine#

You can hand work to another machine (say a small Linux box called rex) and steer it from here. The thread runs there as an ordinary thread of that machine; this one keeps only a link to it. rex never has to reach this computer, so a Mac that sleeps or sits behind NAT is fine. Nothing is ever pushed to your Git remote: the project's state goes over Tau's own encrypted connection as a Git bundle, and the work comes back as a branch.

ADR 0027 explains why this machine's host keeps keys of its own for rex.

The web client#

A listening host also serves a browser client, so a phone or a second machine can supervise the same threads as the desktop window. Build it once (npm run build:web writes dist-web/), then start a host that listens:

npm run build && npm run build:web
TAU_WORKSPACE=/path/to/project TAU_HOST_LISTEN=127.0.0.1:7788 node dist-electron/main/headless.js

Besides the socket line, a host started by hand prints a pairing link (a window's own host and a service do not; create their links in Settings → Connections):

web client: http://127.0.0.1:7788/#pair=<code>&host=<id>&name=<machine> (single use, 10 minutes; allow the device in Settings → Connections or here)

With TAU_HOST_TLS=1 the page and the socket are served over HTTPS on the same port, the link starts with https://, and it carries the pin of the certificate's key (pk=). A browser shows a self-signed certificate as a warning; its fingerprint should match the tls fingerprint the host printed.

Open it. The code lives in the URL's fragment, so it reaches neither a proxy nor an access log, and the page replaces the address before it renders anything. The page does not get in by itself: it asks the host, shows six digits, and waits. The host's owner sees "<device> wants to connect" in every Tau window that holds the host token (or, for a host started by hand, on its terminal) with the same digits, and allows the device only if they match (ADR 0024). The browser then gets a token of its own, never the host token, and keeps it in localStorage. Without a link, "Ask to connect" sends the same request; the host's owner can also paste the host token, the line in ~/.tau/host-token on the host machine. A token the host refuses, one whose access was revoked, or one unused past its timeout, closes the socket and brings the page back with the reason.

Who may connect#

Settings → Connections in a Tau window manages who else may connect. It shows the addresses the host listens on and its certificate fingerprint, the devices waiting to be allowed (with their digits), and makes more pairing links (a label, 10 minutes to a day, Full or Read only, single use; Copy link, and a QR code when the address is reachable from another device; the link names every address and the fingerprint, for the app). It lists the paired devices (browser, OS, address, when each was last active, the last thing it changed, when it will be signed out unused), lets you rename one, make it Read only (it may look, and every change is refused) or Full, pick when it is signed out (30, 90 or 365 days unused, or never), revoke one or all others, which closes their open connections at once. "Rotate…" replaces the host token and disconnects every other connection that used it (a browser paired before tokens of their own, say); paired devices keep theirs. A paired device cannot use the page: managing access takes the host token.

Network access#

Network access on the same page lets the app's own host take other devices, with no environment variables and no restart. Two switches, off by default, combine: Local network listens on every interface, Tailscale only on the machine's Tailscale addresses, so the port stays closed on the LAN. Both use a fixed port (7788 unless you change it) and speak TLS only: the self-signed certificate, or one of your own (Use Own…, a certificate and key such as tailscale cert writes). Tau reads that certificate again when its files change, so a renewal needs no restart; Reload does it at once. Tailscale also opens a plain listener on 127.0.0.1:7789 for a proxy on this machine, such as tailscale serve; everything that arrives through it counts as a remote device, although it comes from 127.0.0.1. The page lists every address a device may use, labelled LAN, .local, Tailscale, MagicDNS or IPv6, and a pairing link carries all of them. Turning a switch off closes its listener and every connection that came through it. A host you start by hand opens a proxy listener with TAU_HOST_PROXY_LISTEN=127.0.0.1:<port>. The installed app ships the web client.

While Local network is on, Tau also announces itself with Bonjour (_tau._tcp), so the Tau app on a phone and other machines on the same network find it without a link. The record carries the host's id, its key and certificate fingerprints and nothing secret; a device found this way still waits until you allow it with matching digits. Turn off Announce on this network to be found only by link or QR code. Find Machines… lists the Tau hosts nearby; it looks only when you ask. macOS may ask once whether Tau may use the local network. Linux needs Avahi (avahi-utils and a running avahi-daemon); Windows 10 1809 or later uses its own mDNS through PowerShell.

Tailscale HTTPS, in the Tailscale section below, has tailscale serve answer at https://<machine>.<tailnet>.ts.net/ in your tailnet with a certificate every browser trusts, and forward to that proxy listener; neither switch has to be on. It needs MagicDNS and HTTPS certificates turned on in the Tailscale admin console, and the section says so while they are off. Before anything changes Tau asks, and says what it costs: every certificate is written to the public Certificate Transparency logs, so the machine's name becomes public for good. Rename the machine first if the name says too much. Serve keeps forwarding after Tau quits (the address answers with an error then); turning the switch off removes only Tau's path. On Linux, tailscale serve needs root or an operator: run sudo tailscale set --operator=$USER once. Tau never runs tailscale funnel, so nothing is published to the internet.

What the browser shows#

The client is the same workbench: the same transcript, composer, thread list, Pi dialogs and Agents panel, reading the same stores over the same protocol. What differs is what it can draw. A browser has no editor and no Electron window, so contributions that need one are not registered there; Settings → Inspector lists them under "Not on this client", with the extension, the contribution and the clients it does claim (ADR 0016). Their host halves keep running: Workspace Kit still records turn checkpoints for a thread driven from the browser, and the desktop window shows them.

Below 720 px the workbench lays itself out compactly, on any client: the thread list becomes a sheet behind a button in the title bar, the composer sticks to the bottom edge, the dock and the stage step aside, and the start screen becomes the list a supervisor wants: every thread with what it is doing, in the desktop rail's words ("Working 2:14" from the host's start of the run, "Question", "Failed", "Ready"), a tap to open it, and a long press for its actions, Stop the run among them. A Pi confirm is answered in the composer, the way it is on the desktop.

Without TLS the socket is unencrypted and the page is served over plain HTTP, so the host refuses to bind anything but a loopback address. To reach it from a phone, start the host with TAU_HOST_TLS=1, or forward the port over SSH or a tunnel you trust; TAU_HOST_INSECURE=1 is the deliberate exception.

The app for iOS and Android#

mobile/ is a native app built with Capacitor around the same compact client. It keeps several hosts, finds hosts on the local network over Bonjour, and talks to each over a socket of its own native side (URLSession on iOS, OkHttp on Android) that pins the key of the host's self-signed certificate, which a web view cannot. The host keeps that key when it renews the certificate, so a renewal needs no new pairing. The Tailscale Serve address has a certificate a public CA issued; the link marks it, and the app checks it the way a browser would instead of pinning. Tokens live in the Keychain or behind a Keystore key.

Add a host by scanning the QR code in Settings → Connections (or pasting its link), or tap a host listed under "On this network". Either way the host's window asks "<phone> wants to connect" with six digits; allow it only if the phone shows the same ones. With a pinned key the digits depend on it, so something between the two that presents another key cannot make them agree (ADR 0024, ADR 0026). The link names every address of the host, and every connection's hello names the host's current ones, so a host added over Bonjour becomes reachable over Tailscale too; the app races them each time it connects (local network first, then .local, then Tailscale), so it follows a phone from home Wi-Fi to cellular on its own. Coming back to the foreground or to a network makes it check the link at once. More → Hosts in the thread list goes back to the host list. tau://thread?host=<id>&thread=<id> opens a thread of a paired host (for push notifications, which are not built yet).

Building and running it in a simulator is in mobile/README.md; putting it on your own iPhone through TestFlight, signed with your Apple Developer account, in mobile-testflight.md.

This page is docs/hosts.md in Tau's repository.