RPC Socket Transport
The daemon exposes a JSON-RPC 2.0 interface over a local IPC stream, a Unix domain socket on Unix and a named pipe on Windows. This is the primary transport for local clients like zerocode. The HTTP/WS gateway remains for webhooks, the web dashboard, and remote REST consumers.
Endpoint resolution
Each data directory gets its own endpoint, so multiple daemon instances on the
same machine do not collide. The data dir is derived from the config dir
(--config-dir / ZEROCLAW_CONFIG_DIR, or ZEROCLAW_DATA_DIR).
| OS | Default endpoint |
|---|---|
| Linux | <data_dir>/daemon.sock (Unix domain socket) |
| macOS | <data_dir>/daemon.sock (Unix domain socket) |
| Windows | \\.\pipe\zeroclaw-<hash> where <hash> is derived from data_dir |
Override with the ZEROCLAW_SOCKET environment variable on either platform:
sh
export ZEROCLAW_SOCKET=/tmp/my-zeroclaw.sock
zeroclaw daemon
PowerShell
$env:ZEROCLAW_SOCKET = '\\.\pipe\my-zeroclaw'
zeroclaw daemon
Wire protocol
NDJSON (newline-delimited JSON). Each line is a complete JSON-RPC 2.0 message. No HTTP framing, no length prefix. The framing is identical across platforms; named pipes carry the same byte stream as Unix sockets.
{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":1},"id":1}\n
{"jsonrpc":"2.0","result":{"protocolVersion":1,"serverVersion":"0.8.5"},"id":1}\n
Handshake
The first RPC call must be initialize. The daemon rejects all other methods
until initialize succeeds. Protocol version mismatch produces a structured
error with code -32011.
{
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"protocolVersion": 1
},
"id": 1
}
The endpoint does not require a pairing token. Access control is handled by the operating system:
- Unix: socket is
0o600, parent directory is0o700. - Windows: named pipe ACL defaults to the creating user and
SYSTEM.
Methods
| Method | Direction | Description |
|---|---|---|
initialize | client -> daemon | Authenticate and negotiate protocol version |
session/new | client -> daemon | Create an agent session (requires agentAlias, optional cwd, sessionId; optional keep_siblings suppresses the idle same-mode sibling eviction for multi-session clients that manage sibling lifecycle themselves) |
session/close | client -> daemon | Close and clean up a session |
session/prompt | client -> daemon | Run a turn (streamed via session/update notifications) |
session/cancel | client -> daemon | Cancel an in-flight turn |
status | client -> daemon | Server version, protocol version, active session list |
session/update | daemon -> client | Streaming notification during a turn (text chunks, tool calls, approvals) |
elicitation/create | daemon -> client | Request interactive input for ask-user and poll flows |
Bidirectional requests
Either side may send a request on the established socket. The receiver must
answer with the same id and exactly one of result or error. Each peer uses
its own zc-out-<number> sequence for outbound requests; the prefix is not a
globally unique namespace. Correlation remains directional: each peer matches
responses only against its own pending-request map, so the same textual ID can
be in flight independently in opposite directions.
{"jsonrpc":"2.0","method":"elicitation/create","params":{"message":"Continue?"},"id":"zc-out-0"}
{"jsonrpc":"2.0","result":{"action":"accept","content":{"answer":"yes"}},"id":"zc-out-0"}
An explicit "result": null is a successful response and still resolves the
pending caller. An error object resolves it as a failure. If the peer does not
answer, the initiating ask-user or poll operation retains its existing timeout
behavior.
Every frame must be a JSON object with "jsonrpc": "2.0". Requests require a
string method; when present, params must be an object or array. Responses
require a string, numeric, or null id and exactly one response member. Invalid
JSON produces -32700 (parse error). A malformed request-shaped envelope
produces -32600 (invalid request), using its valid request ID when one can be
recovered. A malformed response-shaped envelope is logged without frame
contents and dropped without a reply, because echoing its ID could complete an
unrelated request in the opposite direction. Direction-ambiguous envelopes use
id: null. Unknown valid response IDs are likewise logged and ignored rather
than answered, preventing response loops.
Turn streaming
session/prompt returns the final result when the turn completes. During
execution, the daemon sends session/update notifications with incremental
events:
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"...","type":"agent_message_chunk","text":"Hello"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"...","type":"tool_call","toolCallId":"tc_1","name":"bash","rawInput":{...}}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"...","type":"tool_result","toolCallId":"tc_1","name":"bash","rawOutput":"..."}}
Event types: agent_message_chunk, agent_thought_chunk, tool_call,
tool_result, approval_request.
Ephemeral mode
zeroclaw daemon --ephemeral tracks connected clients and self-terminates
when the last one disconnects (after a 1-second grace period). A reconnect
during the grace period cancels the shutdown. The daemon will not exit until
at least one client has connected.
Daemons started without --ephemeral ignore client count and run until
explicitly stopped.
Security
- Unix socket directory:
0o700(owner only) - Unix socket file:
0o600(owner only) - Windows named pipe: default ACL grants the creating user and
SYSTEM SO_PEERCREDon Linux provides the connecting process PID and UID for audit logging; Windows logspipe:localas the peer label
Quick test
Start the daemon in one terminal:
sh
zeroclaw daemon
In a second terminal on Unix, connect with socat:
sh
socat READLINE UNIX-CONNECT:~/.zeroclaw/data/daemon.sock
Paste lines one at a time:
{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":1},"id":1}
{"jsonrpc":"2.0","method":"status","params":{},"id":2}
On Windows, use any named-pipe client (PowerShell [System.IO.Pipes.NamedPipeClientStream],
nc via WSL, or just run zerocode).
Internals
The dispatch layer lives in crates/zeroclaw-runtime/src/rpc/:
| File | Role |
|---|---|
transport.rs | RpcTransport trait |
turn.rs | execute_turn() shared turn executor |
session.rs | RpcSession, SessionStore |
dispatch.rs | RpcDispatcher method routing |
local.rs | LocalTransport + listener (Unix socket / Windows named pipe) |
wss.rs | WSS (WebSocket Secure) transport + TLS acceptor |
attachments.rs | File upload processing, dedup, marker generation |
The RpcTransport trait is designed so that additional transports (vsock,
custom IPC) slot in without touching the dispatch or session logic. The
local.rs module wraps the Unix and Windows primitives behind a single
LocalTransport struct using tokio::io::split, so the read/write loop is
shared across both platforms.