# Server configuration

> Every zidane-server command, flag, environment variable and server.json key with its default, generated from source.

The site build generates the tables from the server's source: the help screen, the configuration loader and the Docker image. Each setting comes from the first source that has it:

1. a flag of `zidane-server start`,
2. a `ZIDANE_SERVER_*` environment variable,
3. a key in `<data dir>/server.json`,
4. the default.

The server validates everything at start and names the source of a bad value. It never reads the generic `PORT` variable.

```text
zidane server: Invalid ZIDANE_SERVER_PORT: expected a port (an integer from 0 to 65535).
```

## Commands

`zidane server <command>` and `zidane-server <command>` are the same program.

| Command | Description |
| - | - |
| `zidane server start [flags]` | Serve this machine's chat host to remote windows. |
| `zidane server token create --name <name>` | Create a device token (printed once). |
| `zidane server token list` | List tokens (never their secrets). |
| `zidane server token revoke <id\|name>` | Revoke a token. Its live connections drop. |
| `zidane server token generate` | Print a fresh token without storing it (for ZIDANE_SERVER_BOOTSTRAP_TOKEN). |
| `zidane server pair` | Print a one-time code that logs a browser in to the web app. |
| `zidane server machine invite` | Print a one-time join code that adds a machine (--name, --ttl). |
| `zidane server machines [--json]` | List the machines and whether they are online (over the unix socket). |
| `zidane server health [--timeout <ms>]` | Probe the running server over its unix socket (exit 0 when healthy). |
| `zidane server health --http` | Probe GET /healthz on the server's port instead (no socket needed). |
| `zidane server wait-idle [--timeout <ms>]` | Wait until no run or background task is in flight (before a restart). |
| `zidane server backup --out <file\|->` | Archive the server's state as a tar.gz while it runs (- for stdout). |
| `zidane server setup` | Install or change zidane-server on this machine (zidane server setup --help). |
| `zidane server doctor [--fix]` | Check a server that setup installed. |
| `zidane server channels check` | Check the channels config offline (exit 1 on errors). |
| `zidane server channels status` | List the running server's channel integrations. |
| `zidane server channels reload` | Make the running server re-read its channels config. |
| `zidane server start --no-channels` | Never start channels (as ZIDANE_NO_CHANNELS=1). |
| `zidane-server --version` | Print the version (the standalone binary). |

## Start flags

| Flag | Description |
| - | - |
| `--host <addr>` | Interface to listen on (default 127.0.0.1). Also `ZIDANE_SERVER_HOST` or `host` in `server.json`. |
| `--port <n>` | Port (default 4280, 0 picks a free one). Also `ZIDANE_SERVER_PORT` or `port` in `server.json`. |
| `--tls-cert <file>` | PEM certificate: serve wss\:// (with --tls-key). Also `ZIDANE_SERVER_TLS_CERT` or `tlsCert` in `server.json`. |
| `--tls-key <file>` | PEM private key. Also `ZIDANE_SERVER_TLS_KEY` or `tlsKey` in `server.json`. |
| `--behind-proxy` | A TLS-terminating proxy fronts the server (required off loopback without TLS). Every upgrade must then carry X-Forwarded-Proto: https. Also `ZIDANE_SERVER_BEHIND_PROXY` or `behindProxy` in `server.json`. |
| `--trusted-proxies <l>` | With --behind-proxy: the proxies' addresses / CIDRs (comma-separated). Upgrades from any other peer are refused. Also `ZIDANE_SERVER_TRUSTED_PROXIES` or `trustedProxies` in `server.json`. |
| `--name <name>` | Host name shown to clients (default: the machine's). Also `ZIDANE_SERVER_NAME` or `name` in `server.json`. |
| `--socket <path\|off>` | Unix socket for local tools (default <data dir>/server.sock). Also `ZIDANE_SERVER_SOCKET` or `socket` in `server.json`. |
| `--workspace-root <dir>` | Where session folders may be. Repeatable, the first is the default (default <data dir>/workspaces, env: path-delimited list). Also `ZIDANE_SERVER_WORKSPACE_ROOTS` or `workspaceRoots` in `server.json`. |
| `--allow-as-machine` | Lend this server as a machine to its clients' computers (local@<server>, docs/MACHINES.md): a client token then runs commands in the workspace roots through it, as it already can with a terminal. Default off. Also `ZIDANE_SERVER_ALLOW_AS_MACHINE` or `allowAsMachine` in `server.json`. |
| `--no-remote-installs` | Refuse installs, updates and removals of skills, extensions and MCP servers from clients (allowRemoteInstalls, default on, audited). Also `ZIDANE_SERVER_ALLOW_REMOTE_INSTALLS` or `allowRemoteInstalls` in `server.json`. |
| `--no-clone` | Refuse clones into the workspace roots from clients (allowClone, default on: any client token may clone). Also `ZIDANE_SERVER_ALLOW_CLONE` or `allowClone` in `server.json`. |
| `--no-create-folders` | Refuse new folders in the workspace roots from clients (allowCreateFolders, default on: any client token may create one). Also `ZIDANE_SERVER_ALLOW_CREATE_FOLDERS` or `allowCreateFolders` in `server.json`. |
| `--auto-update` | Install new releases on its own (release binaries): runs pause at a step and continue after the restart. Default off. Also `ZIDANE_SERVER_AUTO_UPDATE` or `autoUpdate` in `server.json`. |
| `--web` | Serve the web app at / (log in with a code from "zidane server pair"). Browsers on the server's own origin are admitted. Default off. Also `ZIDANE_SERVER_WEB` or `web` in `server.json`. |
| `--no-web` | Don't serve the web app (overrides ZIDANE_SERVER_WEB / server.json). |
| `--web-root <dir>` | The web app to serve (a directory with an index.html, default the one bundled with the server, web-dist/). Also `ZIDANE_SERVER_WEB_ROOT` or `webRoot` in `server.json`. |
| `--mcp` | Serve MCP at /mcp, so an MCP client (Claude Code, Cursor) drives this server's sessions with a token as the bearer. Default off. Also `ZIDANE_SERVER_MCP` or `mcp` in `server.json`. |
| `--no-mcp` | Don't serve MCP (overrides ZIDANE_SERVER_MCP / server.json). |
| `--verbose` | Log connections and requests to stderr. Also `ZIDANE_SERVER_VERBOSE` or `verbose` in `server.json`. |
| `--exposure <mode>` | private (default) or public. Public refuses remote installs, machine lending, a bootstrap token, other browser origins next to the web app, and pairing codes that live longer than 10 minutes. Also `ZIDANE_SERVER_EXPOSURE` or `exposure` in `server.json`. |
| `--channels-port <n>` | Also serve channel webhooks and /healthz alone on this port (same host and TLS, 0 picks a free one). Everything else there is a 404. Also `ZIDANE_SERVER_CHANNELS_PORT` or `channelsPort` in `server.json`. |
| `--env-file <file>` | Read KEY=value lines into the environment before the config loads. Variables already set win. The file must be private (chmod 600). |

## Token and health flags

| Flag | Description |
| - | - |
| `--data-dir <dir>` | Storage root, as ZIDANE_STORAGE_DIR (server files live in <dir>/.zidane). Also ZIDANE_SERVER_DATA_DIR. |
| `--scope <scope>` | Token scope. A window: chat (sessions, runs and tool approvals) or admin (also the server's settings: everything, the default, also named "client"). Or machine: lends its execution with "zidane machine share" (docs/MACHINES.md). For pair: chat or admin (default chat). |
| `--expires <duration>` | (token create) How long the token works: 12h, 30d, 90d, 1y (also m and w), or never (the default). An expired token is refused and its connections drop. |
| `--ttl <duration>` | (pair, machine invite) How long the code works: 30s to 1h, as 90s, 10m or 1h (a bare number is minutes). Default 10m. |
| `--name <label>` | Names the browser's token "browser: <label>". (machine invite) Names the machine's token. Without it, the machine gives its own name. For `pair`. |
| `--json` | Print JSON: { serverVersion, machines }. For `machines`. |
| `--socket <path>` | The server's unix socket, when not the default (health, wait-idle, machines). |
| `--out <file\|->` | Where the tar.gz goes (mode 0600, not inside a directory it archives). - writes it to stdout. For `backup`. |
| `--include-workspaces` | Also archive the workspace roots (the projects). For `backup`. |
| `--http` | Probe http(s)://<host>:<port>/healthz (a wildcard host on loopback). For `health`. |

## Environment variables

An Image value is what `server/Dockerfile` sets. The other variables keep their default in the image.

| Variable | Default | Description |
| - | - | - |
| `ZIDANE_SERVER_DATA_DIR` | Zidane's user dir (image: `/data`) | Storage root, as ZIDANE_STORAGE_DIR (server files live in <dir>/.zidane). |
| `ZIDANE_SERVER_HOST` | `127.0.0.1` (image: `0.0.0.0`) | Interface to listen on. Off loopback it needs TLS or `behindProxy`. |
| `ZIDANE_SERVER_PORT` | `4280` | Port to listen on; `0` picks a free one. |
| `ZIDANE_SERVER_TLS_CERT` | None | PEM certificate: serve wss\:// (with --tls-key). |
| `ZIDANE_SERVER_TLS_KEY` | None | PEM private key. |
| `ZIDANE_SERVER_BEHIND_PROXY` | `false` (image: `1`) | A TLS-terminating proxy fronts the server: every upgrade must then carry `X-Forwarded-Proto: https`. |
| `ZIDANE_SERVER_NAME` | The machine's hostname | Host name reported to clients. Default: the machine's. |
| `ZIDANE_SERVER_HEARTBEAT_MS` | `20000` | Interval of the server's WebSocket pings, in ms; a connection that misses two in a row is dropped. `0` disables. |
| `ZIDANE_SERVER_MAX_MESSAGE_BYTES` | `33554432` | Largest incoming message in bytes: a bigger frame closes the connection (1009), a bigger reply is answered with a `ResponseTooLarge` error. |
| `ZIDANE_SERVER_RATE_LIMIT` | `50` | Requests per second on one connection; `0` disables. |
| `ZIDANE_SERVER_RATE_BURST` | `200` | Burst size of that limit: how many may arrive at once. |
| `ZIDANE_SERVER_ALLOWED_ORIGINS` | None | Browser origins allowed to connect; empty refuses every browser. |
| `ZIDANE_SERVER_SOCKET` | `<data dir>/server.sock` | Unix socket for local tools (default <data dir>/server.sock). |
| `ZIDANE_SERVER_TRUSTED_PROXIES` | None (image: `private`) | Behind a proxy: the only peers believed (IPs, CIDRs, or `private`); empty believes any peer. |
| `ZIDANE_SERVER_UPGRADE_RATE_LIMIT` | `5` | Upgrades per second from one client address; `0` disables. |
| `ZIDANE_SERVER_UPGRADE_RATE_BURST` | `30` | Burst size of that limit: how many may arrive at once. |
| `ZIDANE_SERVER_MAX_CONNECTIONS_PER_TOKEN` | `16` | Open connections one token may hold; past it, an upgrade gets a 429. |
| `ZIDANE_SERVER_MAX_CONNECTIONS_PER_ADDRESS` | `32` | Open authenticated connections per client address; past it, an upgrade gets a 429. |
| `ZIDANE_SERVER_WORKSPACE_ROOTS` | `<data dir>/workspaces` (image: `/data/workspaces`) | Where session folders may be (absolute; the first is the default for a session created without one). Default `<data dir>/workspaces`. |
| `ZIDANE_SERVER_DRAIN_TIMEOUT_MS` | `8000` | Bound on draining the host's runs at shutdown (SIGTERM). |
| `ZIDANE_SERVER_ALLOW_AS_MACHINE` | `false` | Lend this server as a machine to its clients' local hosts (`local@<server>`, `docs/MACHINES.md` § Reverse direction). Default off. |
| `ZIDANE_SERVER_ALLOW_REMOTE_INSTALLS` | `true` | Clients may install, update and remove skills, extensions and MCP servers in this server user's scope (audited). Default on. |
| `ZIDANE_SERVER_ALLOW_CLONE` | `true` | Clients may clone repositories into the workspace roots (`git.clone`). Default on. |
| `ZIDANE_SERVER_ALLOW_CREATE_FOLDERS` | `true` | Clients may create folders in the workspace roots (`fs.mkdir`). Default on. |
| `ZIDANE_SERVER_VERBOSE` | `false` | Log every connection, authentication, refusal and disconnect to stderr (`[server]` and `[ipc-ws]` lines). Default off. |
| `ZIDANE_SERVER_COMPRESS_LARGE_MESSAGES` | `false` | Compress big outgoing WebSocket messages (64 KiB and up, e.g. transcript pages) for clients that accept it (permessage-deflate). Saves bandwidth on slow links, costs event-loop time per big message. Default off. |
| `ZIDANE_SERVER_AUTO_UPDATE` | `false` | Install new releases on its own, pausing and resuming runs (`./auto-update`). Release binaries only. |
| `ZIDANE_SERVER_AUTO_UPDATE_INTERVAL_MINUTES` | `30` | How often it checks for a release. |
| `ZIDANE_SERVER_WEB` | `false` | Serve the web app at `/` (a pairing code logs a browser in: `zidane server pair`), and admit browsers on the server's own origin. Default off. |
| `ZIDANE_SERVER_WEB_ROOT` | None | The web app's directory (it must hold an `index.html`). Default: the app bundled with the server (`web-dist/`). Relative paths in `server.json` resolve against the data dir. |
| `ZIDANE_SERVER_MCP` | `false` | Serve MCP at `/mcp` (Streamable HTTP, a token as the bearer): an MCP client (Claude Code, Cursor, another agent) drives this server's sessions with the `sessions` and `interactions` tools. Default off. |
| `ZIDANE_SERVER_EXPOSURE` | `private` | `public`: the server is reachable from the internet. Start then refuses remote installs, machine lending, a bootstrap token, and other browser origins next to the web app; `pair` caps a code's lifetime at 10 minutes. Default `private`. |
| `ZIDANE_SERVER_CHANNELS_PORT` | None | A second listener on the same host that serves only channel webhooks and `/healthz`; `0` picks a free port. |
| `ZIDANE_SERVER_BOOTSTRAP_TOKEN` | None | A token from `token generate`, stored at start as the client token "bootstrap" (a new value replaces it). Revoke "bootstrap" once device tokens exist. |
| `ZIDANE_SERVER_BOOTSTRAP_TOKEN_FILE` | None | A file holding it (a mounted secret, which keeps it out of the environment). Not both. |

`zidane-server start --no-channels`, or `ZIDANE_NO_CHANNELS=1`, never starts [channels](https://zidane.si/docs/channels.md). The `channels check`, `status` and `reload` commands are in [channels](https://zidane.si/docs/channels.md#commands).

Booleans are `1`, `true`, `yes`, `on` or `0`, `false`, `no`, `off`. Lists are comma-separated (workspace roots are separated like `PATH`). An empty variable counts as unset.

## server.json

`<data dir>/server.json` uses JSON types (numbers, booleans, arrays), not strings. An unknown key is a boot error. Relative paths resolve against the data dir.

| Key | Type | Default | Description |
| - | - | - | - |
| `host` | `string` | `127.0.0.1` | Interface to listen on. Off loopback it needs TLS or `behindProxy`. |
| `port` | `number` | `4280` | Port to listen on; `0` picks a free one. |
| `tlsCert` | `string` | None | PEM certificate: serve wss\:// (with --tls-key). |
| `tlsKey` | `string` | None | PEM private key. |
| `behindProxy` | `boolean` | `false` | A TLS-terminating proxy fronts the server: every upgrade must then carry `X-Forwarded-Proto: https`. |
| `name` | `string` | The machine's hostname | Host name reported to clients. Default: the machine's. |
| `heartbeatMs` | `number` | `20000` | Interval of the server's WebSocket pings, in ms; a connection that misses two in a row is dropped. `0` disables. |
| `maxMessageBytes` | `number` | `33554432` | Largest incoming message in bytes: a bigger frame closes the connection (1009), a bigger reply is answered with a `ResponseTooLarge` error. |
| `rateLimit` | `{ perSecond, burst }` | `perSecond: 50, burst: 200` | Requests per connection: a token bucket of `perSecond` (`0` disables) and `burst`. Over it, a `RateLimited` error. |
| `allowedOrigins` | `string[]` | None | Browser origins allowed to connect; empty refuses every browser. |
| `socket` | `string` | `<data dir>/server.sock` | Unix socket for local tools (default <data dir>/server.sock). |
| `trustedProxies` | `string[]` | None | Behind a proxy: the only peers believed (IPs, CIDRs, or `private`); empty believes any peer. |
| `upgradeRateLimit` | `{ perSecond, burst }` | `perSecond: 5, burst: 30` | Upgrades per client address. |
| `maxConnectionsPerToken` | `number` | `16` | Open connections one token may hold; past it, an upgrade gets a 429. |
| `maxConnectionsPerAddress` | `number` | `32` | Open authenticated connections per client address; past it, an upgrade gets a 429. |
| `workspaceRoots` | `string[]` | `<data dir>/workspaces` | Where session folders may be (absolute; the first is the default for a session created without one). Default `<data dir>/workspaces`. |
| `drainTimeoutMs` | `number` | `8000` | Bound on draining the host's runs at shutdown (SIGTERM). |
| `allowAsMachine` | `boolean` | `false` | Lend this server as a machine to its clients' local hosts (`local@<server>`, `docs/MACHINES.md` § Reverse direction). Default off. |
| `allowRemoteInstalls` | `boolean` | `true` | Clients may install, update and remove skills, extensions and MCP servers in this server user's scope (audited). Default on. |
| `allowClone` | `boolean` | `true` | Clients may clone repositories into the workspace roots (`git.clone`). Default on. |
| `allowCreateFolders` | `boolean` | `true` | Clients may create folders in the workspace roots (`fs.mkdir`). Default on. |
| `verbose` | `boolean` | `false` | Log every connection, authentication, refusal and disconnect to stderr (`[server]` and `[ipc-ws]` lines). Default off. |
| `compressLargeMessages` | `boolean` | `false` | Compress big outgoing WebSocket messages (64 KiB and up, e.g. transcript pages) for clients that accept it (permessage-deflate). Saves bandwidth on slow links, costs event-loop time per big message. Default off. |
| `autoUpdate` | `boolean` | `false` | Install new releases on its own, pausing and resuming runs (`./auto-update`). Release binaries only. |
| `autoUpdateIntervalMinutes` | `number` | `30` | How often it checks for a release. |
| `web` | `boolean` | `false` | Serve the web app at `/` (a pairing code logs a browser in: `zidane server pair`), and admit browsers on the server's own origin. Default off. |
| `webRoot` | `string` | None | The web app's directory (it must hold an `index.html`). Default: the app bundled with the server (`web-dist/`). Relative paths in `server.json` resolve against the data dir. |
| `mcp` | `boolean` | `false` | Serve MCP at `/mcp` (Streamable HTTP, a token as the bearer): an MCP client (Claude Code, Cursor, another agent) drives this server's sessions with the `sessions` and `interactions` tools. Default off. |
| `exposure` | `string` | `private` | `public`: the server is reachable from the internet. Start then refuses remote installs, machine lending, a bootstrap token, and other browser origins next to the web app; `pair` caps a code's lifetime at 10 minutes. Default `private`. |
| `channelsPort` | `string` | None | A second listener on the same host that serves only channel webhooks and `/healthz`; `0` picks a free port. |

```json title="server.json"
{
  "host": "0.0.0.0",
  "behindProxy": true,
  "trustedProxies": ["10.0.1.0/24"],
  "name": "build-box",
  "workspaceRoots": ["/srv/code", "workspaces"],
  "allowedOrigins": ["https://agents.example.com"],
  "rateLimit": { "perSecond": 100, "burst": 400 }
}
```

## Exposure, webhooks and the env file

`exposure` says whether the internet can reach the port of the clients. It is `private` (the default) or `public`. With `public`, the server refuses remote installs, lending itself as a machine (running another host's commands) and a bootstrap token. It also refuses other browser origins next to the web app, and pairing or join codes that live longer than 10 minutes ([exposure](https://zidane.si/docs/server-security.md#exposure)).

`channelsPort` (`--channels-port`, `ZIDANE_SERVER_CHANNELS_PORT`) opens a second port. That port serves channel webhooks and `/healthz` only, and answers 404 to the rest. Make that port public. Keep the port of the clients private.

`--env-file <file>` on `zidane-server start` reads an env file, a file of `KEY=value` lines, before the configuration loads. A variable that is already set wins. The file must be mode 600. `zidane machine share --env-file` reads the `ZIDANE_MACHINE_*` variables of a machine the same way ([servers and machines](https://zidane.si/docs/servers-and-machines.md)).

## Data dir

`--data-dir <dir>` (or `ZIDANE_SERVER_DATA_DIR`) sets the storage root, like `ZIDANE_STORAGE_DIR` for the rest of zidane ([folders](https://zidane.si/docs/files.md#base-folders)). Everything, sessions included, lands in `<dir>/.zidane`. Without it, the server uses zidane's usual folders for its user. The image sets `/data` ([file list](https://zidane.si/docs/server-deploy.md#data-directory-and-backups)).

## Provider keys

The server's agents read provider keys from its environment, under the same names as on a laptop ([the list](https://zidane.si/docs/providers.md#every-provider)). A key saved from a window into the server's `credentials.json` wins over the variable. The variables stay in the agents' environment, so their tools can read them.
