API
Drive pm7Code without the GUI
Most of the time you work with pm7Code in its macOS workspace. But the same agent engine runs behind a small, long-lived remote motor that speaks a WebSocket API — full control over sessions and the event stream. For one-shot prompts in scripts, cron, and CI, use the pm7code CLI instead.
Architecture
One motor, two ways in
The remote motor is a long-lived process that owns sessions. It is deliberately decoupled from any single connection, so a dropped socket never cancels a running turn — you can disconnect and reconnect without losing work. You reach it either through the CLI or straight over its WebSocket API.
The motor runs
A WebSocket server owns sessions and streams a per-session event log. Locally, the CLI starts it for you on demand.
You connect
The pm7code CLI for one-shot prompts, or your own WebSocket client for full session control.
A turn runs
init a session, send a chat turn, read the streamed events, and collect the final answer.
pm7code command itself — options, environment variables, and exit codes — is documented in the main documentation.API
The WebSocket API
When one-shot prompts are not enough — you want to keep a session open, read the live event stream, answer approvals, or reconnect and replay — connect to the motor directly. It speaks JSON text frames over a single WebSocket. Every message is a small tagged object; the tag field t tells you what it is.
// 1. Open a WebSocket to ws://127.0.0.1:8787, then send the hello FIRST.
// Loopback may send device: null; a remote motor requires a paired device.
{ "t": "hello", "protocolVersion": 2, "capabilities": [], "appVersion": "2.x",
"device": null }
// non-loopback: "device": { "id": "<device-id>", "key": "<device-key>" }
// server -> { "t": "helloAck", "protocolVersion": 2, "capabilities": [...] }
// 2. Start a session. persist:true mints a durable, resumable pm7SessionId.
{ "t": "req", "id": "1", "op": "init",
"params": { "cwd": "/path/to/repo", "agent": "claude-sdk", "persist": true } }
// server -> { "t": "res", "id": "1", "ok": true,
// "result": { "sessionId": "...", "pm7SessionId": "..." } }
// 3. Run one turn. Events stream while it runs; the res arrives at turn end.
{ "t": "req", "id": "2", "op": "chat",
"params": { "sessionId": "...", "text": "your prompt" } }
// server -> { "t": "evt", "sessionId": "...", "seq": 7, "evt": "status", "data": {...} }
// server -> { "t": "res", "id": "2", "ok": true,
// "result": { "finalText": "...", "messageCount": 12, "interrupted": false } }
// 4. Close the session when the turn is done.
{ "t": "req", "id": "3", "op": "close", "params": { "sessionId": "..." } }Request / response
You send { t:"req", id, op, params }; the motor replies with { t:"res", id, ok, result } or { t:"res", id, ok:false, error } using the same id.
Server-pushed events
Alongside responses, the motor pushes { t:"evt", sessionId, seq, evt, data } frames with a monotonic seq, so you can track and replay exactly what you have already seen.
API
Handshake and authentication
Because the motor is long-lived and can outlive an app update, every connection starts with a version handshake so a new client never talks silently to an old motor. The hello must be the first frame you send. The motor answers with a helloAck; on any mismatch it sends a fatalError and closes the socket — fail-fast, with a machine-readable code.
Auth model
Loopback connections (127.0.0.0/8, ::1) are trusted and auth-free, so the local GUI and CLI just work. Any non-loopback connection must carry a paired device credential in the hello's device field. Pairing happens once with a 6-digit single-use code started on the server; the motor stores only a hash of each device key and compares in constant time. Devices can be revoked individually on the server.
| fatalError code | Meaning |
|---|---|
| INCOMPATIBLE | The client and motor protocolVersion differ. Restart the side that is out of date. |
| HANDSHAKE_REQUIRED | A non-hello frame arrived first. The hello must be the very first frame on every connection. |
| AUTH_REQUIRED | A non-loopback connection sent no device credential. Pair the device and send { device: { id, key } } in the hello. |
| AUTH_INVALID | The supplied device key is unknown or was revoked. Pair the device again. |
| PAIRING_INVALID | The pairing claim was rejected — wrong, expired, or already-used code. |
API
Control ops
After the handshake, you drive a session with a handful of ops. Each is a req frame with an op name and params; the motor answers on the same id.
initcwd, agent, model?, permissionMode?, persist?, continueLatest?, resumePm7SessionId?Start a session. Selects the agent runtime and returns the ids you reuse on later ops.
Returns { sessionId, pm7SessionId, cacheSid, resumedFrom }
chatsessionId, text, idempotencyKey?Run one full turn. Blocks until the turn ends; events stream while it runs. The same idempotencyKey never starts a second turn.
Returns { finalText, messageCount, interrupted }
interruptsessionIdAbort the running turn mid-flight. The turn ends with a turnComplete carrying interrupted:true.
Returns { ok }
respondToApprovalsessionId, requestId, decision, message?Answer a permission request that arrived as a permission event (decision: accept or decline).
Returns { ok }
attachsessionId, lastSeenSeqReconnect to a session that outlived the socket. The motor sends a snapshot, replays every event after lastSeenSeq, then resumes live.
Returns snapshot + replay, then live
closesessionIdClose the session. Pending turns are not abandoned silently — close after a turn completes.
Returns { ok }
API
The event stream
While a turn runs, the motor pushes evt frames so you can render progress live. Each frame has the session id, a monotonic seq, an evt kind, and a data payload.
| evt | Meaning |
|---|---|
| cacheDelta | An incremental update to the transcript/cache for the session — the readable run, streamed. |
| status | The agent's current activity (thinking, running a tool, compacting). Carries a status label and optional tool label. |
| turnComplete | The turn finished. data.interrupted is true when it was stopped by an interrupt. |
| permission | A permission request lifecycle. data.phase is requested or resolved; requested carries requestId and toolName. |
attach the motor first sends a snapshot frame (status, lifecycle, the seq range it holds), then replays every event after your lastSeenSeq, and only then resumes live — so a reconnecting client catches up exactly once with no gaps or duplicates.API
Sessions, persistence, and resume
A session is owned by the motor, not by your connection. Pass persist: true on init and the motor mints a durable pm7SessionId that survives a restart. Later you can resume it — by id, or by asking for the latest session in a working directory.
Two ids, two jobs
sessionId is the live handle for ops on the open socket. pm7SessionId is the durable, resumable identity that outlives the process.
Resume server-side
Resume is resolved by the motor, which can see the on-disk sessions. The CLI maps --continue and --resume onto init params so a remote client never needs that disk itself.
Idempotent turns
Send chat with an idempotencyKey to make retries safe: a finished turn replays its result, a running one joins instead of starting a second turn.
Operations
Running the motor
For local use you rarely start the motor by hand — the CLI does it for you. You run it explicitly when you want a remote motor on another machine, reachable over your tailnet.
# Loopback only — local clients connect auth-free
node remote/server.cjs
# Reachable over your tailnet — remote clients must present a paired device key
M2_HOST=0.0.0.0 node remote/server.cjsLoopback by default
With no M2_HOST it binds 127.0.0.1 and device-auth sleeps — only local clients can reach it, exactly like the GUI.
Open it deliberately
Set M2_HOST=0.0.0.0 to reach it over the tailnet. Now every non-loopback client must present a paired device key.
Pairing, not shared secrets
Start pairing on the server itself (loopback-only op) to get a 6-digit single-use code; the claiming device receives its own revocable key. Paired devices live in ~/.pm7-code/devices.json (0600), hashes only.
Looking for how pm7Code behaves inside the workspace — agents, prompts, interactive questions, and the Git Service? See the main documentation.