Skip to main content

ante serve

Server mode runs Ante as a long-lived daemon that external clients can drive through a structured message protocol. This is ideal for building editor integrations, web UIs, or any programmatic workflow that needs to control Ante over time.

Basic usage​

ante serve

This starts the daemon and bridges the JSONL protocol over stdin/stdout. The process stays alive until the client sends a Shutdown operation or closes stdin.

Stdio transport (default)​

ante serve --stdio

The --stdio flag is optional — stdio is the default transport. The daemon reads OpMsg JSON objects from stdin and writes EventMsg JSON objects to stdout, one per line.

Unix socket transport​

ante serve --sock # ~/.ante/run/serve.sock
ante serve --sock /tmp/ante.sock

--sock hosts the protocol on a Unix domain socket, so clients that outlive any single connection can dial the host instead of spawning one each time. With no path, the socket is run/serve.sock inside the Ante home.

Each connection drives its own session. A client's Shutdown ends only that client's connection; the host keeps listening. The process exits on SIGINT or SIGTERM.

note

One host owns a socket path at a time. Ownership is an exclusive lock on a .lock file beside the socket: a second ante serve --sock on the same path refuses to start rather than stealing the path, so a live host's socket is never pulled out from under its clients. A socket file left behind by a crashed host holds no lock and is replaced on the next start, so a stale file needs no manual cleanup.

WebSocket transport​

ANTE_SERVE_TOKEN="your-secret-token" ante serve --ws 127.0.0.1:8080

The --ws flag starts a WebSocket server on the given address. ante serve --ws requires a bearer token provided via the ANTE_SERVE_TOKEN environment variable and refuses to start without one. Clients must send Authorization: Bearer <token> in the upgrade request; missing or invalid tokens are rejected with HTTP 401, and handshakes time out after 10 seconds. Each client gets its own connection and session on the shared host. The server keeps accepting connections until the host receives SIGINT or SIGTERM; a client's Shutdown closes only that connection.

note

The transport flags are mutually exclusive — pick one of --stdio, --sock, or --ws.

caution

The address must be a loopback address (127.0.0.0/8 or ::1). Ante refuses to bind anything else — 0.0.0.0:8080 exits with an error rather than starting. To access a daemon across machines, dial through an SSH tunnel or an authenticating reverse proxy with TLS (wss://).

Read the server transcript​

In v0.2.8, --sock and --ws print a readable transcript to stdout: connections, session starts, messages, tool calls, and turn status. Entries carry a server or client label so concurrent connections can be distinguished. Redirect stdout to keep a local transcript:

NO_COLOR=1 ante serve --sock > serve-transcript.log

Styling is enabled only on a terminal and disabled by any nonempty NO_COLOR. The transcript is best-effort: a slow output pipe can drop transcript entries without holding up protocol traffic. Clients should consume the protocol for complete events. --stdio keeps stdout exclusively for JSONL and does not print this transcript.

How it works​

  1. The host starts and waits for operations on the selected transport
  2. Send a StartSession operation to initialize a session with your chosen model and provider
  3. Send UserInput operations to submit prompts
  4. Receive streaming events — thinking deltas, message deltas, tool calls, approvals — on the same connection
  5. Answer TurnPause with ApprovalResponse for an Approval reason or QuestionResponse for a Question reason. Clients without an interactive UI should start sessions with unattended: true
  6. Send Shutdown (or disconnect) to close the connection gracefully. On stdio that also ends the one-connection process; socket and WebSocket hosts continue serving other clients until signalled

See the Protocol Reference for the complete message catalog and wire format.

Protocol input is read independently from event output, so Interrupt and Shutdown remain responsive even when a peer stops consuming events. Each event write has a 30-second deadline; a stalled or response-flooding peer is disconnected instead of blocking the host.

Example session​

Save this as client.cjs and run node client.cjs with Ante on PATH and Anthropic credentials configured. The client keeps stdin open, waits for SessionStart before submitting its prompt, and sends Shutdown after the turn ends or an error occurs. unattended: true denies calls that require approval and skips structured questions so this non-interactive example can finish.

const { spawn } = require("node:child_process");
const { createInterface } = require("node:readline");

const ante = spawn("ante", ["serve", "--stdio"], {
stdio: ["pipe", "pipe", "inherit"]
});
const lines = createInterface({ input: ante.stdout, crlfDelay: Infinity });
let sequence = 0;
let stopping = false;
const send = (op) => ante.stdin.write(
JSON.stringify({ op, id: `op_${++sequence}` }) + "\n"
);

const fail = (error) => {
console.error(error.message);
process.exitCode = 1;
};
ante.on("error", fail);
ante.stdin.on("error", fail);
ante.on("exit", (code) => {
if (code) process.exitCode = code;
});

lines.on("line", (line) => {
if (!line.trim()) return;
const { event } = JSON.parse(line);
console.log("Event:", event);
if (stopping) return;
if (event.SessionStart) {
send({ UserInput: "explain what this project does" });
} else if (event.TurnEnd || Object.hasOwn(event, "Error")) {
if (event.TurnEnd?.status !== "Completed") process.exitCode = 1;
stopping = true;
send("Shutdown");
ante.stdin.end();
}
});

send({ StartSession: {
model: "claude-sonnet-5-5",
provider: "anthropic",
unattended: true
} });

Rust SDK​

ante-sdk is the Ante client for external programs, so a Rust integration does not have to frame JSONL or manage the host process itself. connect returns a Client — an op sender and an event receiver — over any transport:

use ante_sdk::{ConnectOptions, connect, protocol::{Evt, Op, SessionRequest}};

let mut client = connect("stdio".parse()?, ConnectOptions::default()).await?;

client.send(Op::StartSession(SessionRequest {
model: Some("claude-sonnet-5-5".into()),
provider: Some("anthropic".into()),
unattended: Some(true),
..Default::default()
})).await?;

client.send(Op::UserInput("explain what this project does".into())).await?;

while let Some(event) = client.next_event().await {
println!("{event:?}");
if matches!(event.event, Evt::TurnEnd { .. } | Evt::Error(_)) {
break;
}
}
client.close().await;

An Endpoint names where a host is reachable — never a session. Which session a connection drives is decided by the ops sent over it (StartSession, ResumeSession):

EndpointWhat the client doesHost lifetime
stdioSpawns ante serve --stdio as its own childEnds with the connection — dropping the op sender closes the child's stdin
unix:<path>Dials the socket file of an ante serve --sock hostSomeone else's; the host outlives the connection
ws://<addr>, wss://<addr>Dials an ante serve --ws host, presenting ConnectOptions::token as its bearerSomeone else's; the host outlives the connection

Plain ws:// is dialed only to loopback hosts; remote connections require wss:// (under the opt-in wss feature) so the bearer token never travels across the network in cleartext.

Every path yields the same Client, and a process that hosts sessions itself gets that same type from its host directly — the in-process channel carries the wire types the remote codecs serialize, so nothing a client can observe differs between them.

ConnectOptions is all optional and applies across transports: executable (defaults to ante on PATH), args appended after serve --stdio, cwd, extra env, and token (the bearer token sent when dialing ws:// or wss://). Connecting succeeds at the transport level — the pipe opened, or the socket dialed. There is no greeting, so the first op's reply is the liveness check.

// Dial a host someone else is running
let client = connect("unix:/tmp/ante.sock".parse()?, ConnectOptions::default()).await?;

For a complete client, examples/mini-tui is a one-file chat TUI: it streams MessageDeltas, answers TurnPause approvals with y/n, and sends Interrupt on Esc. Run it with cargo run from that directory.

In SDK 0.2.2, ante_sdk::claude and the legacy child-process stdio API were removed. Ante host stdio connections remain supported through connect(Endpoint::Stdio, options), as in the example above.

Differences from headless mode​

HeadlessServer
LifetimeSingle prompt, then exitsLong-lived, multiple interactions
InputCLI argument or stdin pipeStructured JSONL operations
OutputFormatted text (minimal/human/json)Raw protocol events (JSONL)
TransportN/AStdio (default), Unix socket (--sock), or WebSocket (--ws)
SessionsImplicit — one per invocationExplicit — client sends StartSession
Tool approvalDefaults to yolo; explicit strict/auto denies calls that would promptClient must respond to TurnPause events, or declare the session unattended to deny them

CLI reference​

FlagDescription
--stdioUse JSONL over stdin/stdout (default)
--sock [PATH]Serve the protocol over a Unix domain socket (default: run/serve.sock in the Ante home)
--ws <ADDR>Serve the protocol over WebSocket on a loopback address
--offline-model [MODEL]Load a local or verified model in the shared llama serve router and register local. Accepts a path, discovered filename, or verified model name/URL; omit the value for the default saved with ante offline use
note

The --prompt flag cannot be used with ante serve. Use UserInput operations instead.