How to Use the Herdr Unix Socket API for External Agents

Herdr exposes a newline-delimited JSON RPC interface over a Unix-domain socket that external processes can access by connecting to the client socket path and exchanging JSON messages terminated by newlines.

The ogulcancelik/herdr repository provides a terminal workspace manager that exposes control primitives through a Unix socket API. This interface allows external agents written in any language to query runtime status, manage workspaces, and spawn agents by connecting to a domain socket and speaking a simple JSON protocol.

Locating the Unix Socket Path

The server determines the socket location in src/server/socket_paths.rs (lines 14-34). When an explicit session is active, the code uses session::client_socket_path_for to resolve the path. Otherwise, it derives the client socket from the server-side socket specified in HERDR_SOCKET_PATH by inserting "client" before the ".sock" extension, falling back to the legacy environment variable or the session data directory.

You can also override paths using environment variables:

  • HERDR_SOCKET_PATH – Server-side socket location
  • HERDR_CLIENT_SOCKET_PATH – Client-side socket location

Permission restrictions are enforced at src/server/socket_paths.rs lines 80-83 through the restrict_socket_permissions function, which sets the socket permissions to 0o600 (owner-only read/write). Ensure your external agent runs as the same user that launched Herdr.

Understanding the JSON-RPC Protocol

The wire format consists of newline-delimited JSON objects defined in src/api/schema.rs. Every request must include an id and a method field, while responses return either a SuccessResponse or ErrorResponse.

Key schema types:

  • Request { id, method } – The inbound RPC envelope
  • Method – Enum variants including Ping, WorkspaceCreate, AgentStart, and RuntimeStatus
  • SuccessResponse / ErrorResponse – Server reply structures

Each message terminates with a newline character (\n), and clients should read exactly one line per response.

Connecting from Rust

Herdr ships with a first-party client in src/api/client.rs that handles socket resolution, serialization, and deserialization.

The ApiClient struct provides:

  • ApiClient::local() – Connects to the default client socket for the active session
  • ApiClient::for_target() – Connects to a specific ConnectionTarget
  • request() – Sends a request and returns a ResponseResult
  • subscribe_value() – For streaming event subscriptions

Example of querying runtime status:

use herdr::api::client::ApiClient;
use herdr::api::schema::{Method, Request};

let client = ApiClient::local();
let status_req = Request {
    id: "status".into(),
    method: Method::RuntimeStatus,
};
let status = client.request(status_req).expect("cannot query status");
println!("Herdr version: {}", status.version.unwrap_or_default());

Starting an external agent programmatically:

use herdr::api::client::ApiClient;
use herdr::api::schema::{Method, Request, AgentStartParams};

let client = ApiClient::local();
let start = Request {
    id: "agent-1".into(),
    method: Method::AgentStart(AgentStartParams {
        name: "my-agent".into(),
        cwd: None,
        workspace_id: None,
        tab_id: None,
        split: None,
        focus: true,
        argv: vec!["/usr/bin/bash".into()],
    }),
};
let resp = client.request(start).expect("failed to start agent");
println!("Agent started with ID {}", resp.result["agent_id"]);

Implementing Clients in Other Languages

Because the protocol is language-agnostic, any runtime capable of opening Unix-domain sockets can control Herdr. The procedure mirrors the Rust client: resolve the socket path, write a JSON request ending with newline, and read the response line.

General steps:

  1. Determine the socket path (run herdr socket-path or read HERDR_CLIENT_SOCKET_PATH)
  2. Open a SOCK_STREAM connection to the Unix domain socket
  3. Serialize a JSON object with id, method, and optional params
  4. Append \n and write to the socket
  5. Read one line and parse as JSON

Python implementation example:

import socket
import json
import os

# Determine socket path

sock_path = os.environ.get(
    "HERDR_CLIENT_SOCKET_PATH", 
    "/home/user/.local/share/herdr/sessions/default/herdr-client.sock"
)

with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
    s.connect(sock_path)
    
    # Send Ping request

    req = {"id": "ping-1", "method": "Ping", "params": {}}
    s.sendall((json.dumps(req) + "\n").encode())
    
    # Read response

    resp_line = s.makefile().readline()
    resp = json.loads(resp_line)
    print(f"Response: {resp}")

The request format maps directly to the Rust Request struct, and responses conform to either SuccessResponse (containing a result field) or ErrorResponse (containing an error field).

Handling Server-Side Events

For long-lived connections that receive server-pushed events, use the subscribe_value method available in src/api/client.rs. This maintains the socket connection and deserializes incoming JSON lines as they arrive, enabling real-time monitoring of workspace changes or agent status updates.

Summary

  • Herdr exposes a Unix-domain socket at a path derived from src/server/socket_paths.rs, defaulting to the user data directory with 0o600 permissions.
  • The API speaks newline-delimited JSON defined in src/api/schema.rs, using Request objects and SuccessResponse/ErrorResponse envelopes.
  • Rust developers can use ApiClient::local() from src/api/client.rs to handle connection management and serialization automatically.
  • External agents in any language can implement the protocol by opening the socket, writing JSON terminated with \n, and reading line-oriented responses.

Frequently Asked Questions

Where does Herdr store the Unix socket file?

According to src/server/socket_paths.rs, the socket resides in the user's data directory (typically ~/.local/share/herdr/sessions/default/). The exact path is computed by inserting "client" before the ".sock" extension of the server socket, or by evaluating HERDR_CLIENT_SOCKET_PATH if set.

Can I use the Herdr API from Python or Node.js?

Yes. The Herdr Unix socket API is language-agnostic. Any runtime that supports Unix-domain sockets and JSON can connect to the socket path, write newline-terminated JSON requests, and parse line-oriented responses. The src/api/schema.rs file defines the exact field names required in request and response objects.

Why do I get permission denied when connecting to the socket?

The server calls restrict_socket_permissions in src/server/socket_paths.rs (lines 80-83) to set permissions to 0o600, restricting access to the owner only. Ensure your external agent runs under the same user account that started the Herdr daemon.

How do I discover available API methods?

Inspect the Method enum in src/api/schema.rs to see supported RPC calls such as Ping, WorkspaceCreate, AgentStart, and RuntimeStatus. Each variant corresponds to a specific action the server can perform, with parameters defined in the associated parameter structs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →