How st works

What st runs on a machine, how the apps reach it, what it serves, and how it knows what the agents are doing.

st is the part that runs on each machine. Everything you see in the desktop app, the phone apps or a browser comes from the st on the machine it shows.

One binary, one agent process

st is a single file. It carries what it needs: ttyd, tmux, zellij (and psmux, a tmux for Windows, on Windows) and the web page it serves. They are unpacked into a cache the first time it runs. A machine's own tmux is used ahead of the carried one.

Running st starts an agent process in the background and gives the prompt back. The agent does the work from then on:

  • It puts the terminal on a local port, behind a token.
  • It opens the way in from outside (below).
  • It watches every pane on the tmux server for coding agents.
  • Signed in, it reports to your account: that it is alive, its sessions, the agents' states.

The tmux sessions belong to tmux, not to the agent. st exit stops the agent and the tunnel, and the sessions keep running. An upgrade hands the running share to the new version without dropping it. tmux is the default; zellij and no multiplexer at all are the other choices, see multiplexers.

browser or appthe desktop app, a phone app, or a browser
Cloudflare quick tunnelFreetrycloudflare.com, a new address each run
sophonz relayProquiet-otter-lamp.relay.sophonz.io, an address that stays
your machine — no port open to the outside
proxytokenthe web page, the API, the login
ttydone process for every session
tmux · zellij · shellthe session outlives every connection
browser or app ──https──► Cloudflare quick tunnel (Free) or the sophonz relay (Pro) ──► proxy (token) ──► ttyd ──► tmux | zellij | a shell

ttyd is used as-is, for pty ↔ WebSocket, and listens on loopback only. Everything a client sees — the web page, the API, the login — comes from the proxy in front of it.

Ways in

Where the public address comes from depends on the plan. On Free it is a Cloudflare quick tunnel, and the address changes each time the tunnel starts. On Pro it is the sophonz relay, and the address stays. Either way the machine dials out, so no port is open on it. The machine's tailnet is a third choice (st 0.3.93 or later). st route shows which one a machine uses and changes it.

  • On the same computer. The agent listens on 127.0.0.1. The desktop app talks to this computer's st there. st --no-tunnel stops at this: no public address at all.
  • Through a quick tunnel (Free). st starts cloudflared, which dials out to Cloudflare and gets a trycloudflare.com address. st prints the token and an app link, https://link.sophonz.io/j#<tunnel address>, with a QR code holding that link. The tunnel's address is inside the link, after the #: copy that part to open it directly. With SOPHONZ_QR_DIRECT=1, st shows the tunnel's address as it used to. Nothing listens from outside; the machine made the connection. The address changes when the tunnel restarts, and st reconnects by itself when it drops. Cloudflare offers this path for testing and development, with no uptime guarantee (Ways to run).
  • Through the sophonz relay (Pro). Pro includes a relay server that sophonz runs, in place of the quick tunnel. st opens one WebSocket out to it, and the relay sends the requests for the machine's hostname down that connection, so here too nothing listens from outside. The address is three words, such as https://quiet-otter-lamp.relay.sophonz.io, and it stays the same across restarts. One wildcard certificate covers every machine, and the relay pulls its list of machines from the control plane every 30 seconds, so adding a machine restarts nothing and drops no one. From st 0.4.1 a machine signed in to a Pro account fetches its relay address and token itself; a route you picked and a relay of your own (st relay set) come first, and st relay reset picks the quick tunnel and stays there, leaving a chosen tailnet as it is (Share a terminal). Through the relay, the account link and the apps' machine list work; notifications have not been confirmed there yet. Like Cloudflare with a tunnel, the relay passes the bytes along: terminal traffic is not end-to-end encrypted yet.
  • Through your tailnet. On a machine that is on a tailnet, the first st asks once, before the QR, how phones should reach it. On Tailscale, st serves https://<machine>.<tailnet>.ts.net through tailscale serve when the tailnet has HTTPS certificates; without them it serves plain http://100.x.y.z:<port>, which browsers open but the apps do not. Only devices on the same tailnet get in, so the phone needs the Tailscale app. On Tailscale, the account link, opening from the apps' machine list and notifications do not work yet.
  • Through your account. After st login the machine also has a link that does not change, link.sophonz.io/m/<slug>. It opens only for you, signed in. When you follow it, the control plane asks the machine's agent for a key good for one visit and about a minute, and passes you on to the machine's current address.

The token that opens the terminal never leaves the machine. The control plane stores the machine's current address and nothing that would open it. See Accounts and links.

The desktop app and the phone apps list every machine on the account and open each one this way. Ways to run compares the options.

The web terminal and the API

On that one address, behind the token:

  • The web terminal. A terminal page with a key bar, the Nerd Font glyphs prompts use (cached after the first load), a sidebar of sessions and windows, chat and the agent graph. The browser pages describe it.
  • An API the apps use. Sessions, windows and panes (list, open, new, rename, close), the agents and their questions, answering a question, a pane's conversation as chat and typing into it, documents, machine metrics, agent usage limits and past sessions, a pane's files and git changes (read-only), GitHub Actions runs, upgrading st, and the orchestration graph.

All of it comes from the machine to the client. The control plane is not in the path of the terminal, the chat or the metrics.

Agent states

your machine
agent hookClaude Code, Codex, Gemini CLI, opencode, Pi, Kimi Code and Qwen Code run it on each event
st hookone short event, and it stops; not the transcript, not the prompt
the st agentadds what it reads on the screen
states and questionsworking · waiting · done · error · idle, per pane
the appsread live, straight from the machine
control planenames, states, counts and times only
push to your devicesencryptedthe content is encrypted to each device's key
agent hook ──► st hook ──► the st agent on the machine ──► states and questions; from there to the apps over the machine's address, and as events to the control plane ──► push to your devices
  1. st hooks install adds st hook to the settings of Claude Code, Codex, Gemini CLI, Kimi Code and Qwen Code, and writes a plugin for opencode and an extension for Pi. On each event the agent runs it.
  2. st hook sends one short event to the st agent on the same machine and stops there. Not the transcript and not the prompt.
  3. The agent turns events, and what it reads on the screen, into a state for each pane: working, waiting, done, error or idle. A question it can read becomes a prompt with options.
  4. The apps read those states live from the machine. Signed in, the agent also sends state changes to the control plane, which sends the notifications through Apple and Google.
  5. A notification's content is encrypted on the machine to each device's own key. The control plane and Apple or Google see only names, states, counts and times; that is also all the Live Activity carries.

Without hooks st still reads states from the screen, less exactly. Agents and their state has what each state means and where it shows.

Orchestration on the same pieces

Handing work to other agents adds no new server. It is the same st, using what it already has:

  • A worker is a tmux window (psmux on Windows, where workers are adopted rather than started). st worker start opens a window in the coordinator's session and starts Claude Code, Codex, opencode or Pi in it.
  • The task is typed the way the apps' chat types: into the agent's own input box, never over a prompt.
  • The hooks confirm it. st worker start returns once the worker's hooks show it has started its turn.
  • Who works for whom survives a restart of st, and goes when the pane closes. The run's objective and the workers' reports are kept in ~/.sophonz/orch/<run>/run.json.
  • Another machine is reached through the account, the way the apps open it: the coordinator's st talks to that machine's st, which starts the worker there. That machine has to allow it (st orch accept-remote on). st 0.3.85 or later on both.

Orchestration has the commands.