> ## Documentation Index
> Fetch the complete documentation index at: https://docs.embedder.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Headless server mode

> Drive Embedder programmatically over JSON-RPC to embed the agent in your own editor, automation, or test harness

Embedder can run as a long-lived headless server that speaks JSON-RPC 2.0 over stdio. It is the same interface the [VS Code extension](/integrations/vscode-extension) uses, and it lets you embed Embedder's agent in your own editor, automation, or test harness.

```bash theme={"system"}
embedder --server
```

<Note>
  There are two headless modes. `embedder --server` (this guide) is **client-driven**: your client sends requests and the server responds and streams events. `embedder --daemon` is **autonomous**: it connects out to the Embedder backend and executes queued GitHub and Slack work with no human in the loop. See [Daemon mode](#daemon-mode-autonomous) below and the [GitHub bot setup guide](/integrations/github-bot).
</Note>

## When to use it

Use headless server mode when you want to:

* **Embed the agent in an editor or IDE.** The VS Code extension is exactly this.
* **Automate Embedder** from a script, CI job, or your own tool.
* **Build a custom UI** on top of Embedder's chat and hardware capabilities.

If you just want an interactive terminal session, run `embedder` with no flags instead.

## Before you begin

Make sure you have:

1. **`embedder` on your `PATH`.** Verify with:
   ```bash theme={"system"}
   embedder --version
   ```
2. **A logged-in account.** The server uses your stored credentials. Verify with:
   ```bash theme={"system"}
   embedder status
   ```
   If this prints your account details, you are authenticated. If not, run the interactive CLI once (`embedder`) and complete login, or supply a token via the `EMBEDDER_AUTH_TOKEN` environment variable (see [Authentication](#authentication)).
3. **A JSON-RPC client.** Any language works — you only need to read and write `Content-Length`-framed JSON over the child process's stdio. A complete, dependency-free reference client is included [below](#complete-example-client).

## Start the server

The server reads JSON-RPC **requests** on **stdin** and writes **responses** and **notifications** on **stdout**. Typically you don't run it in a terminal yourself — you spawn it from your client and talk to it over the pipes:

```js theme={"system"}
import { spawn } from "node:child_process";

const server = spawn("embedder", ["--server"], {
  cwd: "/path/to/your/project", // the workspace
  env: process.env,
  stdio: ["pipe", "pipe", "pipe"], // [stdin, stdout, stderr]
});
```

<Warning>
  **stdout carries protocol frames only** — the server never logs to stdout. Diagnostic logs go to a rotating file under the Embedder log directory; in dev builds they are additionally mirrored to stderr. Never parse stderr as protocol, and don't let anything else write to the server's stdout.
</Warning>

<Note>
  **One workspace per process.** The server operates on a single project directory. Launch it with the project as the working directory; you also pass that path explicitly in `initialize`.
</Note>

## The wire protocol

Headless mode speaks **JSON-RPC 2.0** with **LSP-style framing**: each message is a `Content-Length` header, a blank line, then a UTF-8 JSON body.

```text theme={"system"}
Content-Length: 123\r\n
\r\n
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{ ... }}
```

There are three message shapes:

| Shape            | Has `id`? | Has `method`?            | Direction                                                |
| ---------------- | --------- | ------------------------ | -------------------------------------------------------- |
| **Request**      | yes       | yes                      | client → server                                          |
| **Response**     | yes       | no (`result` or `error`) | server → client                                          |
| **Notification** | no        | yes                      | server → client (events: text deltas, tool calls, state) |

You don't need any Embedder package or SDK to talk to the server — the wire format is plain JSON-RPC 2.0. The request, response, and notification shapes in this guide are all you need, and a client in any language works.

## Authentication

The server resolves an auth token in this order:

1. **`EMBEDDER_AUTH_TOKEN`** environment variable, if set.
2. **Stored credentials** written by a prior interactive login.

```js theme={"system"}
const server = spawn("embedder", ["--server"], {
  cwd: workspace,
  env: { ...process.env, EMBEDDER_AUTH_TOKEN: process.env.MY_TOKEN }, // optional
  stdio: ["pipe", "pipe", "pipe"],
});
```

If no token is available, auth-dependent calls fail with `-31008 NotAuthenticated`.

<Warning>
  Authentication happens **asynchronously after** `initialize` returns — a `-31008` right after the handshake usually means you need to wait, not that your credentials are wrong. See [Wait for readiness](#wait-for-readiness).
</Warning>

## Connection lifecycle

The normal startup sequence:

<Steps>
  <Step title="Spawn the server">
    Launch `embedder --server` with the project as the working directory, and wire up `Content-Length` framing on stdin/stdout.
  </Step>

  <Step title="Initialize">
    Send `initialize` — the handshake that exchanges protocol version and client identity. See the parameter tables below.
  </Step>

  <Step title="Wait for readiness">
    Auth and model loading complete **after** the handshake. Wait for them before calling anything that needs your identity or a model. See [Wait for readiness](#wait-for-readiness).
  </Step>

  <Step title="Select a team and project">
    Call `team/select` then `project/select`. This is required to chat — the model runs through a backend proxy that needs the project context. See [the note on teams and projects](#select-a-team-and-project-before-chatting).
  </Step>

  <Step title="Configure">
    Set `autoConfirm/set`, `model/set`, and `mode/set` as desired.
  </Step>

  <Step title="Chat">
    Send `chat/send`, then consume the notification stream until the turn completes.
  </Step>

  <Step title="Shut down">
    End the child's stdin (the server exits on EOF), or send `SIGTERM`.
  </Step>
</Steps>

### `initialize`

Request params:

| Field             | Type   | Notes                                                                                                                                                |
| ----------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `protocolVersion` | string | The protocol version your client targets (for example `"0.7.0"`). On incompatibility the server returns `-31000` with both versions in `error.data`. |
| `workspacePath`   | string | Absolute path to the project directory (match the spawn `cwd`).                                                                                      |
| `clientName`      | string | Free-form identifier for your client.                                                                                                                |
| `clientVersion`   | string | Free-form version string.                                                                                                                            |

Result:

| Field                         | Type           | Notes                                                  |
| ----------------------------- | -------------- | ------------------------------------------------------ |
| `protocolVersion`             | string         | The server's protocol version.                         |
| `serverName`                  | string         | `"embedder-cli"`.                                      |
| `serverVersion`               | string         | The CLI version.                                       |
| `capabilities.supportedModes` | string\[]      | Usually `["act","plan","debug"]`.                      |
| `sessionId`                   | string         | The default session's id.                              |
| `activeInstanceCount`         | number         | How many server instances are live for this workspace. |
| `checkpointsEnabled`          | boolean        | Whether snapshot/rewind is available.                  |
| `checkpointsDisabledReason`   | string \| null | Why checkpoints are off, if applicable.                |

<Warning>
  `initialize` resolving **does not** mean the server is ready to chat. It only completes the handshake. Read the next section.
</Warning>

## Wait for readiness

When `initialize` returns, **authentication and model loading have not happened yet** — they run as background work scheduled after the handshake resolves. If you immediately call `team/list`, `chat/send`, or anything else that needs your identity or a model, you get:

```text theme={"system"}
-31008  "Not authenticated. Wait for initialization to complete."
```

This is **by design**, not an error in your client — the handshake is intentionally fast so a UI can render immediately, and the rest streams in. The error is marked recoverable; you are expected to wait.

There are two ways to know the server is ready:

### Option A: watch `state/update` (push, recommended for UIs)

The server emits `state/update` notifications as it boots. Watch the `sessionStatus` field. It transitions out of `"loading"` once auth resolves:

| `sessionStatus`                                                       | Meaning                                                   |
| --------------------------------------------------------------------- | --------------------------------------------------------- |
| `loading`                                                             | Still authenticating / loading models. **Wait.**          |
| `unauthenticated`                                                     | No valid credentials. **Fatal** — log in.                 |
| `no_plan`, `subscription_past_due`, `evaluation_expired`, `suspended` | Account issue. **Fatal** for chat.                        |
| `select_team`                                                         | Authenticated; no team selected yet.                      |
| `select_project`                                                      | Team selected; no project selected yet.                   |
| `create_project`                                                      | Team has no projects.                                     |
| `ready`                                                               | Fully ready (team and project selected, model available). |
| `error`                                                               | Bootstrap failed.                                         |

Auth and model readiness (`currentModel` set, `sessionStatus` past `loading`) is **necessary but not sufficient** to chat: the model runs through a backend proxy that also requires a **selected project**. In practice, wait for auth and model, then select a team and project (reaching `sessionStatus: ready`) before the first `chat/send`.

### Option B: poll `session/status` (pull, simplest for scripts)

`session/status` returns the full state snapshot at any time. Poll it until a model is available:

```js theme={"system"}
async function waitForReady(rpc, timeoutMs = 60_000) {
  const fatal = new Set([
    "unauthenticated", "error", "no_plan",
    "subscription_past_due", "evaluation_expired", "suspended",
  ]);
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const st = await rpc.request("session/status", {});
    if (fatal.has(st.sessionStatus)) throw new Error(`not usable: ${st.sessionStatus}`);
    if (st.currentModel || st.sessionStatus === "ready") return st;
    await new Promise((r) => setTimeout(r, 700));
  }
  throw new Error("timed out waiting for readiness");
}
```

### Select a team and project before chatting

There's a subtlety worth calling out: `chat/send` **accepts** your message even when no project is selected — but the **turn then fails**, because the model runs through a backend proxy that requires project context. Without a selected project the turn fails with `Invalid headers: Invalid UUID …`. This surfaces as a *failed turn*, not as an error on the `chat/send` call itself.

So in practice, select a team and a project before chatting:

```js theme={"system"}
const { teams } = await rpc.request("team/list", {});
await rpc.request("team/select", { teamId: teams[0].id });
const { projects } = await rpc.request("project/list", {});
await rpc.request("project/select", { projectId: projects[0].id });
```

Pick the project that matches your hardware to give the agent the right context (datasheets, SVD, peripherals); any project satisfies the proxy for a generic chat. Methods like `projectDirectory/*`, `peripheral/*`, and `schematics/*` also require a team and project, and return `-31009` / `-31012` otherwise.

## Send a message and stream the turn

### `chat/send`

| Field            | Type       | Notes                                                                           |
| ---------------- | ---------- | ------------------------------------------------------------------------------- |
| `content`        | string     | The user message (sent to the model).                                           |
| `displayContent` | string?    | Optional user-visible text when `content` contains expanded context.            |
| `mentions`       | string\[]? | File/directory paths to expand into context (resolved against `workspacePath`). |
| `images`         | object\[]? | Base64 image attachments for vision.                                            |
| `mode`           | string?    | Override the agent mode for this turn (`act` / `plan` / `debug`).               |
| `sessionId`      | string?    | Target a specific session (defaults to the active one).                         |

Returns `{ messageId, queued, position? }`. If the agent is already busy, the message is **queued** (`queued: true`) rather than rejected.

### The turn streams as notifications

After `chat/send`, the work happens asynchronously and is reported via notifications (all carry an optional `sessionId`):

| Notification          | Fires when                      | Key fields                                  |
| --------------------- | ------------------------------- | ------------------------------------------- |
| `chat/userMessage`    | Your message is committed       | `messageId`, `content`, `checkpointHash?`   |
| `chat/textDelta`      | Each chunk of assistant text    | `messageId`, `delta`, `isFinal?`, `tokens?` |
| `chat/toolCall`       | A tool is invoked               | `toolCallId`, `toolName`, `args`, `status`  |
| `chat/toolStream`     | A tool emits incremental output | `toolCallId`, `output`                      |
| `chat/toolResult`     | A tool finishes                 | `toolCallId`, `result.{success,summary,…}`  |
| `chat/nestedToolCall` | A sub-agent spawns a tool       | `parentToolCallId`, `toolCallId`, …         |
| `bash/output`         | `bash/execute` output line      | `executionId`, `output`                     |
| `state/update`        | Server state changes            | full snapshot including `chatBusy`          |
| `usage/update`        | Token usage / limits change     | `tokensUsedToday`, `creditsAvailable`       |

### Detect that a turn is complete

Use either signal (the example client uses both):

* **`state/update` → `chatBusy` goes `false`** after having been `true`. This is the authoritative "the turn (including any tool calls) is done" signal.
* **`chat/textDelta` with `isFinal: true`** marks the final text chunk of the turn.

<Tip>
  Add a small grace period (a few seconds) after `chatBusy` flips to `false` to avoid races with trailing notifications.
</Tip>

## Approvals: tools, questions, and plans

By default the agent **pauses for confirmation** before running tools that change files, run shell commands, or touch hardware. A headless client must either auto-confirm or respond to each request.

### Auto-confirm everything (unattended runs)

```js theme={"system"}
await rpc.request("autoConfirm/set", { enabled: true });
```

With auto-confirm on, tools run without prompting and no `tool/confirmRequest` notifications fire. This is the right choice for automation and CI.

### Respond to individual tool confirmations

If auto-confirm is off, the server sends a `tool/confirmRequest` notification:

```json theme={"system"}
{ "toolCallId": "…", "toolName": "writeFile", "displayName": "Write File",
  "details": { "title": "…", "affectedPaths": ["…"], "isDestructive": false } }
```

Respond with `tool/respond`:

| `response` | Effect                                                            |
| ---------- | ----------------------------------------------------------------- |
| `once`     | Approve this call only.                                           |
| `always`   | Approve and stop asking for this tool.                            |
| `reject`   | Deny this call.                                                   |
| `redirect` | Deny with a `message` redirecting the agent (requires `message`). |

```js theme={"system"}
await rpc.request("tool/respond", { toolCallId, response: "always" });
```

### Answer questions the agent asks

The agent can ask a multiple-choice or free-text question via the `question/ask` notification:

```json theme={"system"}
{ "questionId": "…",
  "questions": [ { "question": "…", "header": "…",
                   "options": [ { "label": "Yes", "description": "…" } ],
                   "multiSelect": false } ] }
```

Respond with `question/respond`. `answers` is keyed by each question's `header` (or `question`):

```js theme={"system"}
await rpc.request("question/respond", {
  questionId,
  answers: { "Board variant": { answers: ["nrf9160dk/nrf9160"], isCustom: false } },
});
```

### Approve a plan (plan mode)

In plan mode the agent proposes a plan via the `plan/review` notification (`{ planPath, markdownContent }`). Approve it with `plan/approve { planPath }` to execute.

## Agent modes

| Mode    | Tools                                      | Use                          |
| ------- | ------------------------------------------ | ---------------------------- |
| `act`   | full (read, write, shell, build, flash, …) | execute changes (default)    |
| `plan`  | read-only                                  | produce a plan, no mutations |
| `debug` | full + hardware/debug context              | live debugging workflows     |

Set the mode for the session with `mode/set { mode }`, or per-message via the `mode` field on `chat/send`.

<Warning>
  A mode switch is a **turn boundary** — switching mid-turn restarts the turn with the new tool set.
</Warning>

## Sessions and the message queue

* The server starts with one **default session** (its id is in the `initialize` result). You can run **multiple independent sessions** over one connection: `session/create`, `session/list`, `session/status`, `session/close`. Most methods accept an optional `sessionId` (defaulting to the active session).
* If you `chat/send` while a turn is running, the message is **queued**. Manage the queue with `queue/clear`, `queue/edit`, `queue/delete`, and `queue/sendNow`. A `chat/queued` notification reports the queued message's position.
* Stop an in-flight turn with `chat/stop { clearQueue? }`.

## Conversations, rewind, and snapshots

* **History:** `conversation/list`, `conversation/read`, `conversation/load`, `conversation/delete`, `conversation/rename`, `conversation/threads`.
* **Compression:** `conversation/compress` summarizes a long conversation to reduce tokens (`conversation/compressCancel` aborts it). The server also compresses automatically when needed.
* **Rewind:** when `checkpointsEnabled` is true, each turn snapshots the workspace. `chat/rewindEntries` lists rewindable points; `chat/rewind` / `chat/undo` revert files and conversation; `chat/rewindConfirm` commits a rewind. `turnDiff/get` returns the file diff for a turn.

## Complete example client

A self-contained client in plain JavaScript (runs under **Node 18+** or **Bun**), with no external dependencies — it hand-rolls the `Content-Length` framing. It spawns the server, waits for readiness, enables auto-confirm, sends one message, streams the turn, and exits when the turn completes.

Save it as `embedder-client.mjs` and run:

```bash theme={"system"}
node embedder-client.mjs /path/to/project "your prompt"
```

```js embedder-client.mjs theme={"system"}
#!/usr/bin/env node
import { spawn } from "node:child_process";

const WORKSPACE = process.argv[2] ?? process.cwd();
const PROMPT = process.argv[3] ?? "List the files in this project and summarize it.";
const PROTOCOL_VERSION = "0.7.0"; // the version your client targets

// ---- spawn the headless server ----
const server = spawn("embedder", ["--server"], {
  cwd: WORKSPACE,
  env: process.env, // inherits stored credentials / EMBEDDER_AUTH_TOKEN
  stdio: ["pipe", "pipe", "pipe"],
});
server.stderr.on("data", (b) => process.stderr.write(`[server] ${b}`));

// ---- JSON-RPC framing ----
let buf = Buffer.alloc(0);
let nextId = 1;
const pending = new Map();

function send(msg) {
  const body = JSON.stringify(msg);
  server.stdin.write(`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
}
function request(method, params) {
  const id = nextId++;
  return new Promise((resolve, reject) => {
    pending.set(id, { resolve, reject });
    send({ jsonrpc: "2.0", id, method, params });
  });
}

server.stdout.on("data", (chunk) => {
  buf = Buffer.concat([buf, chunk]);
  while (true) {
    const headerEnd = buf.indexOf("\r\n\r\n");
    if (headerEnd === -1) break;
    const m = /content-length:\s*(\d+)/i.exec(buf.subarray(0, headerEnd).toString("ascii"));
    if (!m) { buf = buf.subarray(headerEnd + 4); continue; }
    const start = headerEnd + 4, end = start + Number(m[1]);
    if (buf.length < end) break;
    const msg = JSON.parse(buf.subarray(start, end).toString("utf8"));
    buf = buf.subarray(end);
    dispatch(msg);
  }
});

function dispatch(msg) {
  if (msg.id !== undefined && (msg.result !== undefined || msg.error !== undefined)) {
    const p = pending.get(msg.id);
    pending.delete(msg.id);
    if (p) msg.error ? p.reject(new Error(JSON.stringify(msg.error))) : p.resolve(msg.result);
  } else if (msg.method) {
    onNotification(msg.method, msg.params ?? {});
  }
}

// ---- turn state + completion detection ----
let sawBusy = false, busy = false, done;
const turnComplete = new Promise((r) => (done = r));
let graceTimer = null;
function maybeComplete() {
  clearTimeout(graceTimer);
  if (sawBusy && !busy) graceTimer = setTimeout(done, 3000);
}

function onNotification(method, p) {
  switch (method) {
    case "chat/textDelta":
      process.stdout.write(p.delta ?? "");
      if (p.isFinal) maybeComplete();
      break;
    case "chat/toolCall":
      if (p.status === "executing") console.error(`\n[tool] ${p.toolName} ${JSON.stringify(p.args)}`);
      break;
    case "chat/toolResult":
      console.error(`[tool ${p.result?.success === false ? "✗" : "✓"}] ${p.result?.summary ?? ""}`);
      break;
    case "tool/confirmRequest": // belt-and-suspenders if auto-confirm is off
      request("tool/respond", { toolCallId: p.toolCallId, response: "always" });
      break;
    case "question/ask": {
      const answers = {};
      for (const q of p.questions ?? [])
        answers[q.header ?? q.question] = { answers: [q.options?.[0]?.label ?? "yes"], isCustom: false };
      request("question/respond", { questionId: p.questionId, answers });
      break;
    }
    case "state/update":
      if (p.chatBusy && !busy) { busy = true; sawBusy = true; }
      else if (!p.chatBusy && busy) { busy = false; maybeComplete(); }
      break;
  }
}

// ---- run ----
const init = await request("initialize", {
  protocolVersion: PROTOCOL_VERSION,
  workspacePath: WORKSPACE,
  clientName: "example-client",
  clientVersion: "1.0.0",
});
console.error(`initialized: ${init.serverName} v${init.serverVersion}, session ${init.sessionId}`);

// wait for auth + model (see "Wait for readiness")
const fatal = new Set(["unauthenticated", "error", "no_plan", "subscription_past_due", "evaluation_expired", "suspended"]);
for (let i = 0; i < 90; i++) {
  const st = await request("session/status", {});
  if (fatal.has(st.sessionStatus)) throw new Error(`not usable: ${st.sessionStatus}`);
  if (st.currentModel || st.sessionStatus === "ready") break;
  await new Promise((r) => setTimeout(r, 700));
}

// select a team + project (required — the model proxy needs the project context)
const { teams } = await request("team/list", {});
if (teams?.[0]) { await request("team/select", { teamId: teams[0].id }); console.error(`team: ${teams[0].name}`); }
const { projects } = await request("project/list", {});
if (projects?.[0]) { await request("project/select", { projectId: projects[0].id }); console.error(`project: ${projects[0].name}`); }

await request("autoConfirm/set", { enabled: true });
const res = await request("chat/send", { content: PROMPT, mode: "act" });
console.error(`\nchat/send accepted: ${res.messageId}\n`);

await turnComplete;
console.error("\n\n[turn complete]");
server.stdin.end(); // server exits on stdin EOF
```

## Protocol reference

The common methods and notifications are documented with their parameters in the sections above; the lists below are the complete surface.

Most methods accept an optional `sessionId` (to target a specific session) and `correlation` (an opaque value the server echoes back on the notifications it triggers, so you can match events to the request that caused them).

<AccordionGroup>
  <Accordion title="RPC methods">
    | Category                         | Methods                                                                                                                                                                                                                                                                                                                                                |
    | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
    | Lifecycle & sessions             | `initialize`, `session/create`, `session/list`, `session/status`, `session/load`, `session/rename`, `session/close`, `session/retry`, `stream/replay`, `turnDiff/get`                                                                                                                                                                                  |
    | Chat                             | `chat/send`, `chat/stop`, `chat/clear`, `chat/snapshot`, `chat/undo`, `chat/rewind`, `chat/rewindEntries`, `chat/rewindConfirm`, `chat/sideQuestion`                                                                                                                                                                                                   |
    | Approvals                        | `tool/respond`, `question/respond`, `plan/approve`                                                                                                                                                                                                                                                                                                     |
    | Configuration                    | `model/set`, `mode/set`, `autoConfirm/set`, `agentTeam/set`                                                                                                                                                                                                                                                                                            |
    | Teams & projects                 | `team/list`, `team/select`, `team/deselect`, `project/list`, `project/select`, `project/deselect`, `project/create`, `project/init`, `projectDirectory/get`, `projectDirectory/set`                                                                                                                                                                    |
    | Catalog, peripherals & platforms | `catalog/list`, `peripheral/list`, `peripheral/add`, `peripheral/addCustom`, `platform/addCustom`                                                                                                                                                                                                                                                      |
    | Conversations                    | `conversation/list`, `conversation/read`, `conversation/load`, `conversation/delete`, `conversation/rename`, `conversation/threads`, `conversation/compress`, `conversation/compressCancel`                                                                                                                                                            |
    | Message queue                    | `queue/clear`, `queue/edit`, `queue/delete`, `queue/sendNow`                                                                                                                                                                                                                                                                                           |
    | Commands & skills                | `command/list`, `command/execute`, `skill/list`, `skill/commandContent`                                                                                                                                                                                                                                                                                |
    | Shell                            | `bash/execute`                                                                                                                                                                                                                                                                                                                                         |
    | Auth & account                   | `auth/startDeviceCode`, `auth/logout`, `usage/status`, `billing/creditPackages`, `billing/checkoutSession`, `billing/purchaseCredits`                                                                                                                                                                                                                  |
    | Pull requests                    | `pr/list`, `pr/review`                                                                                                                                                                                                                                                                                                                                 |
    | MCP servers                      | `mcp/serverList`, `mcp/catalogList`, `mcp/connect`, `mcp/disconnect`, `mcp/setEnabled`, `mcp/remove`, `mcp/addServer`, `mcp/installCatalog`                                                                                                                                                                                                            |
    | Connected model providers        | `connect/providerList`, `connect/providerStartAuth`, `connect/providerPollAuth`, `connect/providerDisconnect`                                                                                                                                                                                                                                          |
    | Hardware arbitration             | `hardwareWorkflow/status`, `hardwareWorkflow/request`, `hardwareWorkflow/release`, `hardwareWorkflow/cancel`, `hardwareWorkflow/clear`, `hardwareWorkflow/pause`, `hardwareWorkflow/resume`, `hardwareWorkflow/moveToFront`, `hardwareWorkflow/forceRelease`                                                                                           |
    | Serial / debug probes            | `serial/listPorts`, `serial/connect`, `serial/disconnect`, `serial/send`, `serial/setConfig`, `serial/detectBaudRate`, `serial/listJlinkDevices`, `serial/setJlinkConfig`, `serial/listMapFiles`, `serial/setItmConfig`, `serial/listOpenocdTargets`, `serial/addTab`, `serial/removeTab`, `serial/selectTab`, `serial/clearOutput`, `serial/getState` |
    | Plotting                         | `plot/startOnPort`, `plot/stopOnPort`, `plot/getActivePorts`, `plot/listSessions`, `plot/loadSession`, `plot/renameSession`, `plot/exportCsv`                                                                                                                                                                                                          |
    | Schematics                       | `schematic/parse`, `schematic/checkVersions`, `schematics/list`, `schematics/parsed`, `schematics/referenceCatalog`                                                                                                                                                                                                                                    |
    | Tracing                          | `trace/sessionStart`, `trace/sessionStop`, `trace/listSessions`, `trace/loadSession`, `trace/queryEvents`, `trace/deleteSession`, `trace/attachElf`, `trace/detachElf`, `trace/resolveAddresses`, `trace/analyticsGet`, `trace/analyticsReport`, `trace/finderQuery`                                                                                   |
    | Hardware captures                | `hardware/captureListSessions`, `hardware/captureLoadSession`, `hardware/captureDeleteSession`                                                                                                                                                                                                                                                         |
    | Misc                             | `bug/report`                                                                                                                                                                                                                                                                                                                                           |
  </Accordion>

  <Accordion title="Notifications">
    Notifications are server → client and have no `id`. Subscribe by `method` name; each carries an optional `sessionId`.

    | Category                        | Notifications                                                                                                                                                                                                                                                             |
    | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | Chat stream                     | `chat/userMessage`, `chat/textDelta`, `chat/queued`, `chat/toolCall`, `chat/toolStream`, `chat/toolResult`, `chat/nestedToolCall`, `chat/agentTeamSnapshot`, `chat/rewound`, `chat/sideQuestion/start`, `chat/sideQuestion/finish`                                        |
    | Approvals                       | `tool/confirmRequest`, `question/ask`, `plan/review`                                                                                                                                                                                                                      |
    | State                           | `state/update`, `usage/update`                                                                                                                                                                                                                                            |
    | Shell                           | `bash/output`                                                                                                                                                                                                                                                             |
    | Serial / hardware               | `serial/data`, `serial/connectionState`, `serial/portsChanged`, `serial/baudDetected`, `serial/tabsChanged`, `serial/outputCleared`, `serial/jlinkChanged`, `serial/itmChanged`, `hardwareWorkflow/changed`, `hardware/capturePublished`, `peripheral/documentProcessing` |
    | Plotting                        | `plot/data`, `plot/config`, `plot/status`, `plot/textData`, `plot/xyConfig`, `plot/warning`, `plot/mathChannels`, `plot/mathChannelsChanged`                                                                                                                              |
    | Tracing                         | `trace/streamStatus`, `trace/eventBatch`, `trace/transportHealth`, `trace/sessionUpdate`, `trace/compatibilityWarning`, `trace/symbolizationChanged`                                                                                                                      |
    | Bluetooth                       | `bluetooth/scanAdvertisement`, `bluetooth/connectionState`, `bluetooth/serviceTreeUpdated`, `bluetooth/characteristicNotification`, `bluetooth/eventLog`                                                                                                                  |
    | Editor / providers / schematics | `editor/contextChanged`, `connect/providerStatusChanged`, `schematics/refresh`                                                                                                                                                                                            |

    <Note>
      **Transient vs replayable.** These high-frequency streaming notifications are **transient** — not buffered, so they cannot be replayed: `bash/output`, `serial/data`, `plot/data`, `plot/textData`, `trace/eventBatch`, `trace/streamStatus`, `trace/transportHealth`, `bluetooth/scanAdvertisement`, `bluetooth/characteristicNotification`. All **other** notifications are sequenced and can be replayed with `stream/replay` after a reconnect.
    </Note>
  </Accordion>

  <Accordion title="Error codes">
    JSON-RPC errors arrive as `{ code, message, data }`, where `data` is `{ category, recoverable, details? }`.

    | Code     | Name                    | Meaning                                                                                  |
    | -------- | ----------------------- | ---------------------------------------------------------------------------------------- |
    | `-32002` | NotInitialized          | Call `initialize` first.                                                                 |
    | `-31000` | ProtocolVersionMismatch | Client/server protocol versions incompatible.                                            |
    | `-31002` | ChatAlreadyInProgress   | A turn is already running.                                                               |
    | `-31003` | ChatNotInProgress       | No active turn to act on.                                                                |
    | `-31004` | InvalidToolCallId       | Unknown `toolCallId` in `tool/respond`.                                                  |
    | `-31005` | InvalidQuestionId       | Unknown `questionId` in `question/respond`.                                              |
    | `-31006` | ModelNotAvailable       | No model available (still loading, or none configured).                                  |
    | `-31007` | InvalidMode             | Unsupported agent mode.                                                                  |
    | `-31008` | NotAuthenticated        | Not logged in, or init not yet complete — see [Wait for readiness](#wait-for-readiness). |
    | `-31009` | NoTeamSelected          | Call `team/select` first.                                                                |
    | `-31010` | InvalidEntityId         | Unknown id (team/project/etc.) or unusable workspace.                                    |
    | `-31011` | ProjectLimitReached     | Project limit reached.                                                                   |
    | `-31012` | NoProjectSelected       | Call `project/select` first.                                                             |
    | `-31013` | ConversationNotFound    | Unknown conversation id.                                                                 |
    | `-31014` | ConversationEmpty       | Conversation has no messages.                                                            |
    | `-31015` | PendingConfirmation     | A confirmation is already outstanding.                                                   |
    | `-31016` | InvalidRedirectMessage  | `redirect` response needs a `message`.                                                   |
    | `-31017` | QueueOverflow           | Message queue is full.                                                                   |
    | `-31018` | OperationTimedOut       | Operation timed out.                                                                     |
    | `-31019` | OperationCancelled      | Operation was cancelled/aborted.                                                         |
    | `-31020` | GhCliNotAvailable       | The `gh` CLI is required but missing.                                                    |
    | `-31021` | FeatureNotAllowed       | Feature not allowed for this account/mode.                                               |
    | `-31050` | SerialError             | Serial port / probe error.                                                               |
    | `-31099` | InternalServerError     | Unexpected server error.                                                                 |
  </Accordion>
</AccordionGroup>

<Note>
  The server is also self-describing at runtime: `initialize` returns its name, version, and protocol version, and every error carries a machine-readable `code` plus `data`. The protocol can change between `embedder` releases — pin to a version and handle `-31000` to detect drift.
</Note>

## Daemon mode (autonomous)

`embedder --daemon` is the other headless mode. Instead of a client driving it over stdio, the daemon **connects out** to the Embedder backend over a WebSocket and autonomously executes queued **GitHub** and **Slack** work items — claiming a task, running the agent to completion in an isolated git worktree (with all tool confirmations auto-approved), then committing, opening a PR, or asking a follow-up question.

| Aspect        | `--server`                      | `--daemon`                       |
| ------------- | ------------------------------- | -------------------------------- |
| Driver        | your client (stdio JSON-RPC)    | the backend (outbound WebSocket) |
| Auth          | user token / stored credentials | `EMBEDDER_API_KEY`               |
| Human in loop | yes (or auto-confirm)           | no                               |
| Trigger       | your `chat/send`                | GitHub/Slack work items          |

To build your own client or editor integration, use `--server`. If you want autonomous PR work, use the GitHub bot integration instead of driving the daemon yourself — see [GitHub bot setup](/integrations/github-bot) for the managed `embedder start daemon` / `embedder monitor` / `embedder stop daemon` flow.

## Troubleshooting

<AccordionGroup>
  <Accordion title="-31008 NotAuthenticated right after initialize">
    Auth loads **after** the handshake. Wait for readiness: poll `session/status` until `currentModel` is set, or watch `state/update` for `sessionStatus`. See [Wait for readiness](#wait-for-readiness).
  </Accordion>

  <Accordion title="-31008 that never clears">
    No valid credentials. Run `embedder status`; log in via the interactive CLI or set `EMBEDDER_AUTH_TOKEN`.
  </Accordion>

  <Accordion title="-31006 ModelNotAvailable">
    You sent `chat/send` before a model loaded. Apply the readiness wait.
  </Accordion>

  <Accordion title="Turn fails with 'Invalid headers: Invalid UUID … received undefined'">
    No project selected — the backend model proxy needs the project-context header. Select a team and project before `chat/send` (see [the note on teams and projects](#select-a-team-and-project-before-chatting)). Note that `chat/send` itself does *not* error; only the turn fails.
  </Accordion>

  <Accordion title="Garbage or parse errors on stdout">
    Your framing reader is misaligned, or something wrote non-protocol bytes to stdout. The server never logs to stdout (logs go to a file, and to stderr in dev builds); treat stdout as protocol-only.
  </Accordion>

  <Accordion title="-31000 ProtocolVersionMismatch">
    Your `protocolVersion` doesn't match this `embedder` build. On mismatch, `error.data` reports the server's expected version — send that. The `initialize` result also returns the server's `protocolVersion`.
  </Accordion>

  <Accordion title="Tool calls hang waiting for approval">
    Enable `autoConfirm/set { enabled: true }`, or respond to each `tool/confirmRequest` with `tool/respond`.
  </Accordion>

  <Accordion title="The turn never finishes in your client">
    Track `state/update.chatBusy` (true → false) and/or `chat/textDelta.isFinal`; the turn can include many tool calls before completing.
  </Accordion>

  <Accordion title="Hardware or serial calls fail with -31009 / -31012">
    Those need a selected team and project. Run `team/select` then `project/select`.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="GitHub bot setup" icon="github" href="/integrations/github-bot">
    Run Embedder autonomously in the background to pick up GitHub work.
  </Card>

  <Card title="Use Embedder in VS Code" icon="code" href="/integrations/vscode-extension">
    The official editor integration, built on this same headless protocol.
  </Card>
</CardGroup>
