Skip to content

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.

ClientToken sent asApprovalsServer settingsSettings syncMachinesReconnects
Desktop appSubprotocolAnswers themEdits themYesYes1 s, doubling to 30 s
TUIAuthorization headerAnswers themEdits themYesYes1 s, doubling to 30 s
zidane attachAuthorization headerAnswers themNoNoNoNo
BrowserSubprotocolNoNoNoNo0.5 s, doubling to 15 s; stops on a refusal
Node clientSubprotocol under Node, header under BunYour codeYour codeYour codeYour codeYour code, or the saved-hosts helpers
ACPDoes 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.

AppWhere
Terminal appSettings › Agent › Sessions › Remote hosts
Desktop appSettings › 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#

WhatSaved serversOne server for this launch
HowTurn remote hosts on, then Settings › Hosts › + add a serverzidane --host wss://… --token-file <file>, or ZIDANE_HOST_URL and ZIDANE_HOST_TOKEN
Kepthosts.json (0600), the token in credentials.json under hosts:<id>In memory. The CLI removes both variables from its environment
SessionsGrouped by host next to This computerEvery 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).

Terminal
zidane attach --host wss://zidane.example.com --token-file box.token

Screens 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).

TypeScript
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
}
ErrorMeans
HostAuthErrorThe upgrade got a 401 or a 403. reason carries the X-Zidane-Refusal code
HostBusyErrorThe 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.

Terminal
# 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 ~/code

zidane 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.

ModeDoes
OffNothing (the default)
Import onceCopies a snapshot now
Keep in syncPushes each change as it happens. Changes made offline go on reconnect

Categories, picked per server:

CategoryCopies
Agent settingsTool approvals, run limits, compaction, interaction mode, subagent models. Never the window's own settings
Safe-mode allowlistsPer project, for projects the server also has (matched by path, then folder name). Others are skipped
SYSTEM.md and user skillsYour 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
ExtensionsThe server installs those that came from npm or GitHub, which needs allowRemoteInstalls. Local ones cannot sync
Provider API keysSecrets, 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.