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.
embedder with no flags instead.
Before you begin
Make sure you have:embedderon yourPATH. Verify with:- 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 theEMBEDDER_AUTH_TOKENenvironment variable (see Authentication). - 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: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 aContent-Length header, a blank line, then a UTF-8 JSON body.
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:EMBEDDER_AUTH_TOKENenvironment variable, if set.- Stored credentials written by a prior interactive login.
-31008 NotAuthenticated.
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:
Wait for readiness
Wheninitialize 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:
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:
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:
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
Afterchat/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/update→chatBusygoesfalseafter having beentrue. This is the authoritative “the turn (including any tool calls) is done” signal.chat/textDeltawithisFinal: truemarks the final text chunk of the turn.
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)
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 atool/confirmRequest notification:
tool/respond:
Answer questions the agent asks
The agent can ask a multiple-choice or free-text question via thequestion/ask notification:
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 theplan/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.
Sessions and the message queue
- The server starts with one default session (its id is in the
initializeresult). You can run multiple independent sessions over one connection:session/create,session/list,session/status,session/close. Most methods accept an optionalsessionId(defaulting to the active session). - If you
chat/sendwhile a turn is running, the message is queued. Manage the queue withqueue/clear,queue/edit,queue/delete, andqueue/sendNow. Achat/queuednotification 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/compresssummarizes a long conversation to reduce tokens (conversation/compressCancelaborts it). The server also compresses automatically when needed. - Rewind: when
checkpointsEnabledis true, each turn snapshots the workspace.chat/rewindEntrieslists rewindable points;chat/rewind/chat/undorevert files and conversation;chat/rewindConfirmcommits a rewind.turnDiff/getreturns 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 theContent-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 optionalsessionId (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).
RPC methods
RPC methods
Notifications
Notifications
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.Error codes
Error codes
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
-31008 NotAuthenticated right after initialize
-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.-31008 that never clears
-31008 that never clears
No valid credentials. Run
embedder status; log in via the interactive CLI or set EMBEDDER_AUTH_TOKEN.-31006 ModelNotAvailable
-31006 ModelNotAvailable
You sent
chat/send before a model loaded. Apply the readiness wait.Turn fails with 'Invalid headers: Invalid UUID … received undefined'
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). Note that chat/send itself does not error; only the turn fails.Garbage or parse errors on stdout
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.
-31000 ProtocolVersionMismatch
-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.Tool calls hang waiting for approval
Tool calls hang waiting for approval
Enable
autoConfirm/set { enabled: true }, or respond to each tool/confirmRequest with tool/respond.The turn never finishes in your client
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.Hardware or serial calls fail with -31009 / -31012
Hardware or serial calls fail with -31009 / -31012
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.
