Reference
Protocol commands & events
Version 1 of the device protocol: nine command types the cloud may send, eleven event types the bridge sends back, and the exact limits of each. If this page and the schema ever disagree, the schema wins.
Canonical source: packages/protocol/src/schemas.ts in the public bridge repository. The private control plane vendors a copy and has a test that fails if the two drift.
Identifiers#
Every id is a prefix, an underscore, and 32 lowercase hex characters. They are opaque. The cloud never sends a filesystem path, a phone number, or a provider-native id as an identifier.
usr_ dev_ proj_ ses_ cmd_ apr_ att_The command envelope#
Every command arrives wrapped in an envelope carrying the signature and the id of the key that produced it.
{ "keyId": "k2026-08", "signature": "<base64url Ed25519 over canonicalize(body)>", "body": { "version": 1, "commandId": "cmd_…", "userId": "usr_…", "deviceId": "dev_…", "issuedAt": "2026-08-24T12:00:00.000Z", "expiresAt": "2026-08-24T12:05:00.000Z", "nonce": "…at least 16 characters…", "idempotencyKey": "…at least 8 characters…", "type": "agent.start_session", "payload": { } }}canonicalize is JSON.stringify with recursively sorted keys, no whitespace and undefined dropped. The verification order the bridge applies is in how the bridge works.
The nine commands#
| Type | What it does | What it cannot do |
|---|---|---|
device.probe | Asks for a fresh picture: agents, projects, sessions. Returns a device.hello payload. | Change anything. It is read-only. |
project.list | Returns the project summaries. | Reveal a filesystem path. Summaries never contain one. |
project.remove | Forgets a project id. | Delete files. It only removes a registry entry. |
agent.start_session | Starts Claude Code or Codex in a project you registered locally, with an instruction of up to 8000 characters, up to four image attachments, and an optional read-only flag. The cloud pre-allocates the session id. | Name a directory. It sends a proj_… id, which only resolves against your local registry — so an unregistered folder is unreachable. |
agent.send_instruction | Sends follow-up text to an existing session in auto, steer or queue mode, with up to four attachments. | Force a steer where the adapter cannot do it. auto falls back to queueing and emits a queued_followup event. |
agent.stop_session | Interrupts a session. | Undo work already written to disk. |
agent.get_status | Returns session summaries, optionally for one session. | Return transcripts, diffs or file contents. Summaries are capped and structured. |
agent.respond_to_approval | Answers a permission prompt the agent raised — carrying the approval id, session id, the provider's request id, the preview hash and the decision. | Answer a different request than the one you saw. All four must match the retained request, and entries are single-use. |
settings.sync_public_policy | Updates the Tier A auto-approve flag and the approval timeout (30–3600 seconds). | Auto-approve Tier B or C. The flag applies to Tier A and nothing else. |
Events#
Every event carries { version, eventId, deviceId, at, inReplyTo? } plus its type and payload.
| Type | When | Payload |
|---|---|---|
device.hello | After each successful authentication | Bridge and macOS version, agent status, project summaries, session summaries |
device.heartbeat | Every 20 seconds | The number of active sessions |
command.ack | Exactly once per received command | Command id, status, optional error code, message and result |
project.registered / project.removed | A local CLI change, or a project.remove command | The project summary, or the id |
agent.connection | An adapter's status changes | Provider, mode, install and auth state, capabilities |
session.updated | A session is created or changes state | The session summary |
session.event | Progress, messages, completion — kinds include started, progress, agent_message, needs_input, completed, failed, stopped, queued_followup, followup_delivered | A summary of at most 2000 characters — never a full transcript or diff |
approval.requested | An agent asked permission | Action type, a preview of at most 1500 characters, its hash, the deterministic hints, and an expiry |
approval.resolved_locally | Timeout, an answer given in the terminal, or shutdown | allowed, denied, timed_out or canceled |
attachment.consumed | After a download attempt | The attachment id and whether it worked — never image bytes |
Acks and error codes#
Every command gets exactly one ack, with one of five statuses:
| Status | Meaning |
|---|---|
accepted / completed | Taken on, and finished. |
rejected | The guard refused it before dispatch. |
failed | Dispatch ran, and the adapter or a precondition failed. |
duplicate | An idempotent retry returned the original result. |
The error codes are a closed set too:
bad_signature expired replayedwrong_device unknown_project unknown_sessioncapability_unsupported provider_error invalid_payloadFrames#
The WebSocket carries JSON objects discriminated by kind. From the bridge: auth.request, auth.response, event, pong. From the gateway: auth.challenge, auth.result, command, ping.
auth.result may carry serverKeys, which the bridge pins and uses to verify commands, and minBridgeVersion — if the running bridge is older it stops reconnecting and logs an update-required error.