Server clients
Each zidane-server client (desktop app, TUI, browser, Node client, machine), how it attaches and what it can do.
Every client that connects to a server speaks the same protocol. The table compares what each client can do.
| Client | Token sent as | Approvals | Server settings | Settings sync | Machines | Reconnects |
|---|---|---|---|---|---|---|
| Desktop app | Subprotocol | Answers them | Edits them | Yes | Yes | 1 s, doubling to 30 s |
| TUI | Authorization header | Answers them | Edits them | Yes | Yes | 1 s, doubling to 30 s |
zidane attach | Authorization header | Answers them | No | No | No | No |
| Browser | Subprotocol | No | No | No | No | 0.5 s, doubling to 15 s; stops on a refusal |
| Node client | Subprotocol under Node, header under Bun | Your code | Your code | Your code | Your code | Your code, or the saved-hosts helpers |
| ACP | Does not connect |
Turn remote hosts on#
The Remote hosts setting turns on saved servers, the Hosts settings, per-host session groups and the host picker of a new session. It is off by default in both apps.
| App | Where |
|---|---|
| Terminal app | Settings › Agent › Sessions › Remote hosts |
| Desktop app | Settings › Advanced › Experimental › Remote hosts |
--host <url> (or ZIDANE_HOST_URL) connects one launch to one server, with or without the setting. ZIDANE_NO_REMOTE=1 (or zidane --no-remote) wins over both. Remote hosts is a window setting and never syncs to a server (setting kinds).
Desktop app#
Add servers under Settings › Accounts › Hosts. The main process of the app stays connected to every saved server. The window never talks to a server directly.
- The sidebar groups sessions by host. A new chat picks its host, then a folder on that host.
- A server session runs there, with the server's folder, files, commands and provider keys (what lives where).
- While a server session is open, Settings edits that server's agent settings, skills, MCP servers and extensions.
- A session is known by its host and id together.
Screens and steps: Hosts and sync.
TUI#
| What | Saved servers | One server for this launch |
|---|---|---|
| How | Turn remote hosts on, then Settings › Hosts › + add a server | zidane --host wss://… --token-file <file>, or ZIDANE_HOST_URL and ZIDANE_HOST_TOKEN |
| Kept | hosts.json (0600), the token in credentials.json under hosts:<id> | In memory. The CLI removes both variables from its environment |
| Sessions | Grouped by host next to This computer | Every session on that server |
The footer shows host: <name>. Both apps read the same hosts.json and tokens. zidane run --json always runs locally and refuses --host.
--token <t> leaks into ps and your shell history. Use --token-file instead (- reads stdin).
zidane attach is a line client for one session. It watches the session, sends prompts and answers approvals (more).
zidane attach --host wss://zidane.example.com --token-file box.tokenScreens and keys: Remote hosts.
Browser#
A web page attaches with createServerHost from zidane-ui/adapters, which backs the kit's chat components (setup). A page cannot answer tool approvals yet (limits).
Underneath is createServerClient from zidane/ipc/browser: the same typed calls and signals as the Node client, in any runtime with a WebSocket (no Node APIs). It negotiates the protocol version, says what the token may call (can), queues calls while it reconnects, resumes from the last signal, and stops on a refusal no retry can fix: a bad or revoked token, an origin or transport the server refuses, a protocol the two don't share. A page can't read why an upgrade was refused, so after a failed connect it asks the server's auth check (GET /zidane/auth).
List the page's origin in ZIDANE_SERVER_ALLOWED_ORIGINS (origins) and give the page its own token with the narrowest scope it needs: any script in the page can read it. examples/ui-server runs a page against a scripted server.
Node client#
connectWsClient from zidane/ipc/remote returns a client typed for every protocol method. It needs a global WebSocket (Node 22 or later, or Bun).
import { connectWsClient, HostAuthError } from 'zidane/ipc/remote'
try {
const client = await connectWsClient({ url: 'wss://zidane.example.com', token: process.env.ZIDANE_HOST_TOKEN })
const stop = client.onSignal((signal, meta) => console.log(signal.kind, meta?.seq))
const session = await client.request('sessions.create', { cwd: '' })
await client.request('chat.send', { sessionId: session.id, prompt: 'Run the tests.' })
stop()
client.close()
}
catch (err) {
if (err instanceof HostAuthError)
console.error(err.status, err.message) // 401: token refused; 403: err.reason says why
else
throw err
}| Error | Means |
|---|---|
HostAuthError | The upgrade got a 401 or a 403. reason carries the X-Zidane-Refusal code |
HostBusyError | The upgrade got a 429: too many connections for the token or the address, or too many attempts. Transient |
IpcVersionMismatchError (from zidane/ipc) | The protocol versions differ |
A redirect fails the connect. The client does not follow a redirect, so the token never goes to another address.
keepHostConnected and startWsSavedHosts add reconnection and the saved-hosts registry the apps use. A refused token or another host stops the retries until retry(). Another protocol version leaves the host version-mismatch, checked again every 5 minutes. Anything else backs off from 1 to 30 seconds. retryAll() reconnects every saved host now, as the desktop app does when the computer wakes. The client strips $seq from signals and passes it as meta.seq.
Under Bun, the client trusts the public certificate authorities and the operating system's store (hostCaBundle()), or the ca you pass. Under Node, call trustSystemCertificates() once to make every TLS client of the process trust the system store, as the desktop app does.
To embed the server in a Bun process, use startZidaneServer from src/server/server.ts (what zidane-server start runs). It is not a package export. examples/ui-server/scripted-server.ts imports it from source, with a token store, an origin and a workspace root.
Machines#
A machine lends a server its folders, shell, background tasks and terminals. It opens the WebSocket itself, so it needs no inbound port.
A machine gets no signals, session data, settings or keys. It can call only $/ping, ipc.hello and machine.register. The server sends it execution calls.
# On the server: a machine-scoped token.
zidane-server token create --name laptop --scope machine
# On the machine:
zidane machine share --host wss://zidane.example.com --token-file ~/.config/zidane/laptop.token \
--name laptop --root ~/codezidane machine share also reads the token from ZIDANE_MACHINE_TOKEN. Both apps' Hosts pages do both steps at once ("share this computer") through tokens.createMachineToken. The server gets a shell on the machine as your user. File operations stay inside the declared folders. Revoking the token drops the machine.
In the other direction, --allow-as-machine lets a server lend itself to its clients' computers. zidane server setup asks for it (default yes, never on a public server). It shows up there as local@<server>, and ipc.hello reports machineServe: true.
Settings sync#
Settings sync copies this computer's settings to one saved server, one way.
| Mode | Does |
|---|---|
| Off | Nothing (the default) |
| Import once | Copies a snapshot now |
| Keep in sync | Pushes each change as it happens. Changes made offline go on reconnect |
Categories, picked per server:
| Category | Copies |
|---|---|
| Agent settings | Tool approvals, run limits, compaction, interaction mode, subagent models. Never the window's own settings |
| Safe-mode allowlists | Per project, for projects the server also has (matched by path, then folder name). Others are skipped |
SYSTEM.md and user skills | Your user-scope SYSTEM.md and skills, as files |
| MCP servers (user scope) | URL servers sync as they are. A stdio server is flagged when its command is missing on the server. Env and headers are copied too |
| Extensions | The server installs those that came from npm or GitHub, which needs allowRemoteInstalls. Local ones cannot sync |
| Provider API keys | Secrets, copied to another machine. Off by default. You opt in each key on its own |
A server takes settings from one device only. Its record in <data dir>/sync.json holds the device, its categories, the fields it set, and the fields edited on the server after that. Sync leaves those edits alone until you reset them. Your computer keeps its side in hosts-sync.json.
MCP servers and extensions arrive through the installs that --no-remote-installs turns off. MCP sign-ins never sync. Sign in on the server from its MCP servers tab, in either app. If another device already syncs to the server, the app names it and offers to take over.
Where the controls are: desktop, terminal.
ACP#
The ACP adapter runs the agent in a process your editor starts and never connects to a server. It shares only the JSON-RPC framing and standard error codes with the server's protocol.