Skip to content

Protocol

Every interactive front end (the terminal UI, the line mode, the web UI, the VS Code extension and the coming mobile apps) is a client of the daemon. They all speak one protocol: JSON messages over a WebSocket, plus a few REST endpoints. This page is a summary for client authors. The full specification, with every field, is docs/protocol.md in the repository; the Rust types are in crates/malac/src/protocol.rs, and crates/malac/src/client.rs is a typed Rust client.

The running daemon describes itself in ~/.malac/daemon.json (mode 0600):

{
"url": "http://127.0.0.1:7700",
"token": "…",
"pid": 68119,
"version": "1.0.0",
"urls": ["http://127.0.0.1:7700/?token=…"],
"started": "2026-10-25T18:33:10.271046Z"
}

The file can outlive a crashed daemon, so treat the daemon as running only when pid is alive and GET {url}/api/daemon answers. To start one, run malac daemon start and wait for the file.

  • Local and remote. A request is local when it comes from a loopback address, names a loopback host (127.0.0.1, localhost or [::1]) in Host, and carries no proxy header (Forwarded, X-Forwarded-For, X-Forwarded-Host, X-Real-IP, CF-Connecting-IP). Everything else is remote: other machines, tunnels, proxies, and pages that rebind their name to this machine. Without remote access, remote requests get 403.
  • Tokens. Every /api request needs a token, as Authorization: Bearer <token> or a token query parameter. The master token from daemon.json works on local requests only; it persists in ~/.malac/daemon.token, and deleting that file rotates it. Other devices pair for a device token, which works local or remote.
  • Origin. WebSocket upgrades are accepted without an Origin (apps, the CLI), from the page’s own origin, and from VS Code webviews.
Thing Id What it is
Workspace UUIDv7 A set of tabs. Saved in ~/.malac/workspaces/<id>.json.
Tab UUIDv7 A project folder with its own runtime and its top-level agents
Agent UUIDv7 A conversation, with a transcript in ~/.malac/sessions/<id>.jsonl. Subagents have a parent.
Job 8 hex digits A background process an agent started. Saved in ~/.malac/jobs/.
Permission request UUIDv7 A tool call waiting for approval

Tabs load lazily: a tab’s runtime is built the first time an agent in it is prompted, started or opened. After a daemon restart, agents are saved sessions (live: false) until prompted.

Connect to ws://127.0.0.1:<port>/api/ws?version=<your version> with Authorization: Bearer <token> (wss:// for a daemon behind TLS). Browsers, which cannot set headers on a WebSocket, pass token=<token> in the query instead. The first message is always a snapshot: the daemon’s state (daemon, workspaces, the connection’s workspace with its tabs and agents, pending approvals, jobs, update, control, models, default_model, remote and awake). After that, every message is a broadcast that all clients get, or a reply to one of your commands. Apply broadcasts to the snapshot to stay current; on {"type": "lagged"}, send snapshot again. A connection’s commands run in order, and the broadcasts a command causes reach it before the command’s reply.

When your version differs from the daemon’s, snapshot.daemon.version_mismatch is true, so you can suggest malac daemon restart.

A command is a JSON object whose type names it. Add a client_ref to get a reply carrying the same value: the command’s own reply, or ack; failures come back as {"type": "error", "message": …}. One connection’s commands run in order.

{ "type": "prompt", "tab": "01a17453-…", "text": "fix the failing test", "client_ref": "c7" }
Group Commands
Agents prompt (to an agent, or a new agent in a tab; with images and attachments), agent_start, agent_open, agent_activate, agent_close, cancel, approve, mode, model, compact
Workspaces and tabs launch (what the command line does: last, new or named workspace, a tab for a folder, a prompt), snapshot, workspace_list, workspace_create, workspace_open, workspace_rename, workspace_delete, tab_create, tab_close, tab_rename, tab_move, tab_activate
Jobs, control and the daemon job_list, job_output, job_kill, control_permissions (with request to show the macOS prompts), keep_awake (busy, always or off, until the daemon restarts), ping

Approvals. permission_requested reaches every client. Any of them may answer with approve (allow, allow_session or deny); the first answer wins and permission_resolved tells everyone. Requests still open when you connect are in snapshot.pending.

Project. A loaded tab carries project: where its folder sits in the repository (root, subdir, siblings), suggest_root when the folder is one part of a larger repository and the client should offer to open the root, and commands, the tab’s custom slash commands, for completion. Send those as ordinary prompt text; the agent expands them, and answers /context itself.

Attachments. prompt takes attachments: [{path}], resolved against the agent’s folder. Text files become a fenced block (cut at 256 KB), images image blocks (up to 10 MB), folders a listing, other files a note. A pasted image has no file the daemon could read, so it goes in images as base64, with a name to show it as an attachment.

  • event: every agent event, tagged with its tab and workspace. The envelope is the same as in JSON events.
  • Changes: workspace_created, workspace_updated, workspace_deleted, tab_created, tab_updated, tab_closed, tabs_reordered, job_started, job_finished, control, mode, clients, remote (a tunnel or a paired device changed), awake (keep-awake changed), notice, reloaded, shutdown.
  • Replies: ack, error, pong, launched, workspaces, workspace_opened, jobs, job_output, control, snapshot.

All under a token except GET /api/hello and POST /api/pair. JSON in and out.

Endpoint What it does
GET /api/daemon Version, pid, clients, workspaces, loaded tabs, running agents and jobs, pending approvals, idle timeout, remote access, keep-awake
POST /api/daemon/stop Stop the daemon; 409 while agents or jobs run, unless {"force": true}
GET /api/snapshot The snapshot, as on the WebSocket
POST /api/command Any command above; returns its reply
GET /api/workspaces, GET /api/jobs, GET /api/jobs/{id}/output Listings and a job’s log tail
GET /api/fs?path=&files= A folder’s subfolders, and with files=true its files, for the pickers; below daemon.browse_root
GET /api/sessions?cwd=&limit=&subagents= Saved sessions, newest first, so a client on another machine can list recents
GET /api/sessions/{id}, DELETE /api/sessions/{id} A transcript, or delete it
GET /api/usage?days= Tokens and cost from the session transcripts: today, the week, the window, per model and per day (default 30 days)
/api/settings, /api/models, /api/tools, /api/memory, /api/update Settings, as the web UI uses them; GET /api/settings includes the tab’s context report
Endpoint What it does
GET /api/hello No token. {app: "malac", version, remote, pairing}, so a client can tell it found a daemon
POST /api/pair {code, name, kind?} No token. Trades a pairing code for a device token: {token, device, version}. 403 for a wrong, used or expired code; 429 after ten wrong codes.
POST /api/pair/codes A new code, valid ten minutes for one device, with a pairing link and QR code (SVG) per address
GET /api/devices, DELETE /api/devices/{id} The paired devices (yours marked current), or revoke one; its sockets close
GET /api/remote Remote access: on or off, the bound address, the addresses others can use, the tunnel, the number of devices
POST /api/tunnel {provider}, DELETE /api/tunnel Start or stop a tunnel

An app pairs in three calls: read the base address and code from the QR code’s link (<base>/#pair=<code>), check GET <base>/api/hello, and POST <base>/api/pair. It keeps the token in the platform’s secure storage and sends it as Authorization: Bearer from then on. A 401 means the device was revoked.

URL=$(jq -r .url ~/.malac/daemon.json)
TOKEN=$(jq -r .token ~/.malac/daemon.json)
# what the daemon is doing
curl -s -H "Authorization: Bearer $TOKEN" "$URL/api/daemon" | jq
# a prompt to a new agent in the active tab of the last workspace; the reply is an ack
curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"type": "prompt", "text": "summarize the README"}' "$URL/api/command"

The agent’s output then streams to every WebSocket client, so open the terminal UI or the web UI to watch it. For a client of your own, read the full protocol or use the Rust client.

malac is created by lobirus and released under the MIT license. About malac