Skip to main content
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 uses, and it lets you embed Embedder’s agent in your own editor, automation, or test harness.
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 below and the GitHub bot setup guide.

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:
  2. A logged-in account. The server uses your stored credentials. Verify with:
    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).
  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.

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

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.
There are three message shapes: 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.
If no token is available, auth-dependent calls fail with -31008 NotAuthenticated.
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.

Connection lifecycle

The normal startup sequence:
1

Spawn the server

Launch embedder --server with the project as the working directory, and wire up Content-Length framing on stdin/stdout.
2

Initialize

Send initialize — the handshake that exchanges protocol version and client identity. See the parameter tables below.
3

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

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

Configure

Set autoConfirm/set, model/set, and mode/set as desired.
6

Chat

Send chat/send, then consume the notification stream until the turn completes.
7

Shut down

End the child’s stdin (the server exits on EOF), or send SIGTERM.

initialize

Request params: Result:
initialize resolving does not mean the server is ready to chat. It only completes the handshake. Read the next section.

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:
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: The server emits state/update notifications as it boots. Watch the sessionStatus field. It transitions out of "loading" once auth resolves: 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:

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:
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

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):

Detect that a turn is complete

Use either signal (the example client uses both):
  • state/updatechatBusy 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.
Add a small grace period (a few seconds) after chatBusy flips to false to avoid races with trailing notifications.

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)

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:
Respond with tool/respond:

Answer questions the agent asks

The agent can ask a multiple-choice or free-text question via the question/ask notification:
Respond with question/respond. answers is keyed by each question’s header (or question):

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

Set the mode for the session with mode/set { mode }, or per-message via the mode field on chat/send.
A mode switch is a turn boundary — switching mid-turn restarts the turn with the new tool set.

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:
embedder-client.mjs

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).
Notifications are server → client and have no id. Subscribe by method name; each carries an optional sessionId.
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.
JSON-RPC errors arrive as { code, message, data }, where data is { category, recoverable, details? }.
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.

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. 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 for the managed embedder start daemon / embedder monitor / embedder stop daemon flow.

Troubleshooting

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.
No valid credentials. Run embedder status; log in via the interactive CLI or set EMBEDDER_AUTH_TOKEN.
You sent chat/send before a model loaded. Apply the readiness wait.
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). Note that chat/send itself does not error; only the turn fails.
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.
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.
Enable autoConfirm/set { enabled: true }, or respond to each tool/confirmRequest with tool/respond.
Track state/update.chatBusy (true → false) and/or chat/textDelta.isFinal; the turn can include many tool calls before completing.
Those need a selected team and project. Run team/select then project/select.

Next steps

GitHub bot setup

Run Embedder autonomously in the background to pick up GitHub work.

Use Embedder in VS Code

The official editor integration, built on this same headless protocol.
Last modified on July 10, 2026