29 KiB
herdr socket api
herdr exposes a local unix socket api for scripts, tools, and coding agents that want to control a running herdr instance or subscribe to events.
if you are teaching an agent that is already running inside herdr, start with SKILL.md. use this document when you want the direct protocol, or when you want the cli wrapper reference for the commands that sit on top of it.
choose your integration layer
there are three practical ways to integrate with herdr:
- agent skill —
SKILL.md. best when an agent inside herdr just needs to learn the workflow quickly. - cli wrappers —
herdr server stop,herdr workspace ...,herdr tab ...,herdr agent ...,herdr pane ...,herdr wait .... best for shell scripts and simple orchestration. - raw socket api — best when you want direct request/response control or long-lived event subscriptions.
these layers are intentionally stacked on top of the same control surface.
important difference: pane.run and wait agent-status are cli conveniences, not raw socket methods.
transport
- transport: unix domain socket
- encoding: newline-delimited json
- request/response: send one json request per line, read one json response per line
- subscriptions: send
events.subscribe, receive an ack, then keep the same connection open and continue reading pushed events
named sessions are runtime/socket namespaces, not replacements for herdr workspaces. each named session has its own server sockets and persistent runtime state while config remains global.
socket path resolution order:
- explicit
herdr --session <name>:$XDG_CONFIG_HOME/herdr/sessions/<name>/herdr.sockor$HOME/.config/herdr/sessions/<name>/herdr.sock HERDR_SOCKET_PATHHERDR_SESSION=<name>:$XDG_CONFIG_HOME/herdr/sessions/<name>/herdr.sockor$HOME/.config/herdr/sessions/<name>/herdr.sock- default session path:
$XDG_CONFIG_HOME/herdr/herdr.sockor$HOME/.config/herdr/herdr.sock
this means HERDR_SOCKET_PATH remains an exact low-level socket override, but an explicit cli --session <name> still wins when a command runs inside a pane that inherited HERDR_SOCKET_PATH.
session names may contain ASCII letters, numbers, ., _, and -. default is reserved for the default session. use herdr session list, herdr session attach <name>, herdr session stop <name>, and herdr session delete <name> to inspect and manage session namespaces. session commands print human-readable output by default; pass --json for machine-readable output. session delete refuses running sessions and does not delete the default session.
request and response envelopes
all socket requests use this envelope:
{
"id": "req_1",
"method": "ping",
"params": {}
}
successful responses look like:
{
"id": "req_1",
"result": {
"type": "pong",
"version": "0.1.2",
"protocol": 2
}
}
errors look like:
{
"id": "req_1",
"error": {
"code": "pane_not_found",
"message": "pane 1-99 not found"
}
}
ids and numbering
workspace ids are opaque, stable ids like:
w64e95948145ed1w64e95948146a82
pane ids are workspace-scoped and stable across workspace reorder:
w64e95948145ed1-1w64e95948145ed1-2w64e95948146a82-1
that means:
- workspace id = stable workspace identity
- pane number = compact pane number within that workspace
workspace ids are durable for the life of the workspace and survive display reordering. pane numbers are still compact public numbers, so if a pane closes, higher pane numbers in that same workspace compact down.
tabs are first-class socket api objects now.
- tab ids look like
w64e95948145ed1:1,w64e95948145ed1:2 - workspace id = stable workspace identity
- tab number = tab number within that workspace
- pane ids still stay workspace-scoped like
w64e95948145ed1-2rather than becomingworkspace-tab-panetriples
for backward compatibility, requests also accept the older positional forms like 1, 1:2, and 1-2 as shorthand for the current session order. responses use the stable ids.
core objects
workspace_info responses contain objects like:
{
"workspace_id": "w64e95948145ed1",
"number": 1,
"label": "herdr",
"focused": true,
"pane_count": 1,
"tab_count": 1,
"active_tab_id": "w64e95948145ed1:1",
"agent_status": "unknown"
}
tab_info responses contain objects like:
{
"tab_id": "w64e95948145ed1:1",
"workspace_id": "w64e95948145ed1",
"number": 1,
"label": "1",
"focused": true,
"pane_count": 1,
"agent_status": "unknown"
}
pane_info responses contain objects like:
{
"pane_id": "w64e95948145ed1-1",
"terminal_id": "term_64e95948145ed1",
"workspace_id": "w64e95948145ed1",
"tab_id": "w64e95948145ed1:1",
"focused": true,
"cwd": "/home/can/Projects/herdr",
"label": "reviewer",
"agent": "pi",
"agent_status": "working",
"custom_status": "indexing",
"revision": 0
}
terminal_id is an opaque terminal identity. during the pane-backed transition each pane has one terminal id, but clients should not derive it from the pane id.
label is an optional manual pane name set through pane.rename.
agent_info responses contain a terminal-facing view of a live agent terminal:
{
"terminal_id": "term_64e95948145ed1",
"name": "reviewer",
"agent": "pi",
"agent_status": "working",
"workspace_id": "w64e95948145ed1",
"tab_id": "w64e95948145ed1:1",
"pane_id": "w64e95948145ed1-1",
"focused": true,
"cwd": "/home/can/Projects/herdr",
"revision": 0
}
name is an optional unique agent alias set by agent.start or agent.rename. it is separate from the pane label: pane.rename names a terminal/pane for UI purposes, while agent.rename declares or changes the agent-facing name and rejects duplicate active agent names.
agent is an optional display label string.
- when herdr detects a built-in agent, this is that built-in name like
piorclaude - when a hook or plugin reports a custom agent through
pane.report_agent, this can be any non-empty label likehermes - when no agent identity is known, it is omitted
custom_status on pane_info and agent-status events is an optional hook-owned display label. It is present only while hook authority is active. It does not change agent_status, done semantics, waits, notifications, or workspace/tab rollup priority.
pane_read responses contain objects like:
{
"pane_id": "w64e95948145ed1-1",
"workspace_id": "w64e95948145ed1",
"tab_id": "w64e95948145ed1:1",
"source": "recent",
"format": "text",
"text": "...",
"revision": 0,
"truncated": false
}
agent_status is the public agent field:
idleworkingblockeddoneunknown
done means the agent has finished, but you have not looked at that finished pane yet.
methods at a glance
| method | purpose | success result type |
|---|---|---|
ping |
health check / version | pong |
server.stop |
gracefully stop the running background server | ok |
workspace.list |
list workspaces | workspace_list |
workspace.get |
inspect one workspace | workspace_info |
workspace.create |
create a workspace | workspace_info |
workspace.focus |
focus a workspace | workspace_info |
workspace.rename |
rename a workspace | workspace_info |
workspace.close |
close a workspace | ok |
tab.list |
list tabs, optionally filtered by workspace | tab_list |
tab.get |
inspect one tab | tab_info |
tab.create |
create a tab in a workspace | tab_info |
tab.focus |
focus a tab | tab_info |
tab.rename |
rename a tab | tab_info |
tab.close |
close a tab | ok |
agent.list |
list terminal-backed agents | agent_list |
agent.get |
inspect one agent by terminal id, unique agent name, detected agent label, or pane id | agent_info |
agent.read |
read output from one agent terminal | pane_read |
agent.send |
send literal text to one agent terminal | ok |
agent.rename |
set or clear the unique agent name | agent_info |
agent.focus |
show the agent terminal in the TUI | agent_info |
agent.start |
start a named agent terminal from argv | agent_started |
pane.list |
list panes, optionally filtered by workspace | pane_list |
pane.get |
inspect one pane | pane_info |
pane.rename |
set or clear a manual pane label | pane_info |
pane.read |
read pane output | pane_read |
pane.split |
split a pane and create a sibling pane | pane_info |
pane.send_text |
send literal text without Enter | ok |
pane.send_keys |
send keypresses like Enter |
ok |
pane.send_input |
send literal text plus keypresses in order | ok |
pane.report_agent |
report hook-authoritative agent label and state for a pane | ok |
pane.clear_agent_authority |
clear hook-authoritative agent state for a pane | ok |
pane.release_agent |
release a pane from the reported agent back to shell state | ok |
pane.close |
close a pane | ok |
pane.wait_for_output |
one-shot blocking wait for text | output_matched |
events.subscribe |
start a long-lived subscription stream | subscription_started ack |
server.stop
request:
{
"id": "req_stop",
"method": "server.stop",
"params": {}
}
returns ok and asks the running background server to shut down cleanly.
this is the explicit server-level shutdown path for persistence mode. normal in-app quit actions detach the current client instead of sending this request.
workspace methods
workspace.list
request:
{
"id": "req_list",
"method": "workspace.list",
"params": {}
}
returns workspace_list with zero or more workspace objects.
workspace.get
params:
{
"workspace_id": "1"
}
returns workspace_info for one workspace.
workspace.create
params:
{
"cwd": "/home/can/Projects/herdr",
"focus": true
}
notes:
cwdis optional- if
cwdis omitted, herdr uses its current working directory and falls back to/if needed focusis optional in raw socket requests and defaults tofalse- the cli wrapper also defaults to no focus; pass
--focusto switch to the new workspace
example response:
{
"id": "req_create",
"result": {
"type": "workspace_info",
"workspace": {
"workspace_id": "1",
"number": 1,
"label": "herdr",
"focused": true,
"pane_count": 1,
"tab_count": 1,
"active_tab_id": "1:1",
"agent_status": "unknown"
}
}
}
workspace.focus
params:
{
"workspace_id": "1"
}
returns the focused workspace as workspace_info.
workspace.rename
params:
{
"workspace_id": "1",
"label": "api"
}
returns updated workspace_info.
workspace.close
params:
{
"workspace_id": "1"
}
returns:
{
"id": "req_close",
"result": {
"type": "ok"
}
}
tab methods
tab.list
request with no filter:
{
"id": "req_tabs",
"method": "tab.list",
"params": {}
}
request filtered to one workspace:
{
"id": "req_tabs_ws",
"method": "tab.list",
"params": {
"workspace_id": "1"
}
}
returns tab_list.
tab.get
params:
{
"tab_id": "1:2"
}
returns tab_info.
tab.create
params:
{
"workspace_id": "1",
"cwd": "/home/can/Projects/herdr",
"focus": true
}
notes:
workspace_idis optional and defaults to the active workspacecwdis optional; if omitted, herdr uses the focused pane cwd in that workspace when availablefocusis optional in raw socket requests and defaults tofalse- the cli wrapper also defaults to no focus; pass
--focusto switch to the new tab
returns tab_info for the new tab.
tab.focus
params:
{
"tab_id": "1:2"
}
returns focused tab_info.
tab.rename
params:
{
"tab_id": "1:2",
"label": "logs"
}
returns updated tab_info.
tab.close
params:
{
"tab_id": "1:2"
}
returns ok. the last tab in a workspace cannot be closed.
pane methods
pane.list
request with no filter:
{
"id": "req_panes",
"method": "pane.list",
"params": {}
}
request filtered to one workspace:
{
"id": "req_panes_ws",
"method": "pane.list",
"params": {
"workspace_id": "1"
}
}
returns pane_list.
pane.get
params:
{
"pane_id": "1-1"
}
returns pane_info.
pane.rename
params:
{
"pane_id": "1-1",
"label": "reviewer"
}
send label: null or omit label to clear the manual pane label.
returns pane_info.
pane.read
params:
{
"pane_id": "1-1",
"source": "recent",
"lines": 80,
"format": "text",
"strip_ansi": true
}
notes:
sourceis required and must bevisible,recent, orrecent_unwrappedlinesis optional- current implementation defaults to
80lines whenlinesis omitted and caps reads at1000 formatdefaults totext; useansifor a rendered VT/ANSI snapshot with styles preservedstrip_ansidefaults totrueand is kept for compatibility
source meanings:
visible— current viewportrecent— recent scrollback textrecent_unwrapped— recent scrollback text with soft wraps joined
example response:
{
"id": "req_read",
"result": {
"type": "pane_read",
"read": {
"pane_id": "1-1",
"workspace_id": "1",
"tab_id": "1:1",
"source": "recent",
"format": "text",
"text": "...",
"revision": 0,
"truncated": false
}
}
}
pane.split
params:
{
"target_pane_id": "1-1",
"direction": "right",
"focus": true
}
notes:
directionmust berightordowncwdis optionalfocusis optional in raw socket requests and defaults tofalse- the cli wrapper also defaults to no focus; pass
--focusto switch to the new pane
returns pane_info for the new pane.
pane.send_text
params:
{
"pane_id": "1-1",
"text": "bun run dev"
}
this sends literal text only. it does not press Enter.
pane.send_keys
params:
{
"pane_id": "1-1",
"keys": ["Enter"]
}
use this after pane.send_text when you want to submit a command.
pane.send_input
params:
{
"pane_id": "1-1",
"text": "bun run dev",
"keys": ["Enter"]
}
this sends text plus encoded keypresses in order within one request. when bracketed paste is enabled in the pane, the text portion is sent as a paste payload before the keys. use this when you need text + Enter to behave more like a real keypress sequence than pane.send_text with a literal trailing \r.
text and keys are both optional, but at least one should usually be present.
pane.report_agent
use this when an agent hook or plugin wants to report a semantic state directly over the socket api.
params:
{
"pane_id": "1-1",
"source": "custom:hermes",
"agent": "hermes",
"state": "working",
"message": "running tools",
"custom_status": "indexing"
}
notes:
sourceis required and identifies the reporting integration instanceagentis required and may be any non-empty label string- built-in names like
piare normalized to their public label form - custom labels like
hermesare accepted as-is - while this authority is active, the reported
agentandstateoverride heuristic display for that pane - process detection still owns pane liveness and fallback when hook authority is cleared or released
messageis optional metadata for the reporting integrationcustom_statusis an optional short display label such asscheduled,indexing, orstuckcustom_statusis visual-only; usestatefor semantic behavior like working or blocked- omitting
custom_statusclears any previous custom status from this reporting source
returns ok.
pane.clear_agent_authority
params:
{
"pane_id": "1-1",
"source": "custom:hermes"
}
notes:
sourceis optional- when
sourceis omitted, any hook authority for that pane is cleared - when
sourceis present, only that reporting source is cleared
returns ok.
pane.release_agent
use this when the reported agent is exiting cleanly and wants herdr to drop agent identity immediately instead of waiting for fallback detection.
params:
{
"pane_id": "1-1",
"source": "custom:hermes",
"agent": "hermes"
}
notes:
agentuses the same non-empty label rules aspane.report_agent- this clears the pane's effective agent identity immediately when the source and label match the active authority
- for built-in detected agents, herdr also applies its normal short reacquire suppression during graceful release
returns ok.
pane.close
params:
{
"pane_id": "1-2"
}
returns ok.
waits
pane.wait_for_output
this is the direct socket-side one-shot blocking wait.
params:
{
"pane_id": "1-1",
"source": "recent",
"lines": 200,
"match": { "type": "substring", "value": "ready" },
"timeout_ms": 30000,
"strip_ansi": true
}
matcher forms:
{ "type": "substring", "value": "ready" }
{ "type": "regex", "value": "server.*ready" }
notes:
sourcemust bevisible,recent, orrecent_unwrappedlinesis optionaltimeout_msis optionalstrip_ansidefaults totrue- for
source = "recent", output matching uses unwrapped recent terminal text so soft wraps do not break matches source = "recent_unwrapped"is also available onpane.readwhen you want to inspect the same unwrapped transcript directly- on success you get
output_matched - on timeout you get an error response with code
timeout
example success response:
{
"id": "req_wait",
"result": {
"type": "output_matched",
"pane_id": "1-1",
"revision": 0,
"matched_line": "server ready",
"read": {
"pane_id": "1-1",
"workspace_id": "1",
"tab_id": "1:1",
"source": "recent_unwrapped",
"text": "...server ready...",
"revision": 0,
"truncated": false
}
}
}
subscriptions
events.subscribe is the long-lived pubsub entrypoint.
you send a subscribe request once, get an ack on the same connection, and then keep reading newline-delimited json events from that same socket.
subscription ack
{
"id": "sub_1",
"result": {
"type": "subscription_started"
}
}
supported subscriptions
base lifecycle subscriptions:
workspace.createdworkspace.closedworkspace.focusedtab.createdtab.closedtab.focusedtab.renamedpane.createdpane.closedpane.focusedpane.exitedpane.agent_detected
parameterized subscriptions:
pane.output_matchedpane.agent_status_changed
event naming rule
this part matters because the pushed event names are not all shaped the same.
- when you subscribe to a base lifecycle event, the pushed
eventvalue uses snake_case with underscores:- subscribe with
workspace.created - receive
workspace_created
- subscribe with
- when you subscribe to a parameterized subscription, the pushed
eventvalue keeps the dotted name:- subscribe with
pane.output_matched - receive
pane.output_matched
- subscribe with
examples below show both forms.
example: subscribe to lifecycle events
request:
{
"id": "sub_life",
"method": "events.subscribe",
"params": {
"subscriptions": [
{ "type": "workspace.created" },
{ "type": "workspace.focused" },
{ "type": "tab.created" },
{ "type": "tab.focused" },
{ "type": "tab.renamed" },
{ "type": "tab.closed" },
{ "type": "pane.created" },
{ "type": "pane.focused" },
{ "type": "pane.agent_detected" },
{ "type": "pane.closed" },
{ "type": "workspace.closed" }
]
}
}
example pushed event:
{
"event": "workspace_created",
"data": {
"workspace": {
"workspace_id": "1",
"number": 1,
"label": "herdr",
"focused": true,
"pane_count": 1,
"tab_count": 1,
"active_tab_id": "1:1",
"agent_status": "unknown"
}
}
}
example: subscribe to output matches and agent status changes
request:
{
"id": "sub_1",
"method": "events.subscribe",
"params": {
"subscriptions": [
{
"type": "pane.output_matched",
"pane_id": "1-1",
"source": "recent",
"lines": 200,
"match": { "type": "substring", "value": "ready" }
},
{
"type": "pane.agent_status_changed",
"pane_id": "1-1",
"agent_status": "done"
}
]
}
}
notes:
pane.output_matchedsupportssource, optionallines, matcher config, and optionalstrip_ansipane.agent_status_changedaccepts an optionalagent_statusfilter; if omitted, any status transition for that pane can match
example pushed pane.output_matched event:
{
"event": "pane.output_matched",
"data": {
"pane_id": "1-1",
"matched_line": "server ready",
"read": {
"pane_id": "1-1",
"workspace_id": "1",
"tab_id": "1:1",
"source": "recent_unwrapped",
"text": "...server ready...",
"revision": 0,
"truncated": false
}
}
}
example pushed pane.agent_status_changed event:
{
"event": "pane.agent_status_changed",
"data": {
"pane_id": "1-1",
"workspace_id": "1",
"agent_status": "done",
"agent": "pi",
"custom_status": "scheduled"
}
}
agent in pushed events follows the same rules as pane_info.agent: it may be a built-in detected name, a custom hook-reported label, or omitted. custom_status may be included when the pane has hook-owned custom status metadata at the time of the semantic status transition.
cli wrappers
these commands provide the shell-facing control surface. most command groups talk to the local socket; status client only inspects the local executable.
command groups
status commands:
herdr status
herdr status server
herdr status client
herdr -V and herdr --version print the local executable version without contacting the server. herdr status compares that local executable with the running server when one is reachable.
workspace commands:
herdr workspace list
herdr workspace create [--cwd PATH] [--label TEXT] [--focus] [--no-focus]
herdr workspace get <workspace_id>
herdr workspace focus <workspace_id>
herdr workspace rename <workspace_id> <label>
herdr workspace close <workspace_id>
tab commands:
herdr tab list [--workspace <workspace_id>]
herdr tab create [--workspace <workspace_id>] [--cwd PATH] [--label TEXT] [--focus] [--no-focus]
herdr tab get <tab_id>
herdr tab focus <tab_id>
herdr tab rename <tab_id> <label>
herdr tab close <tab_id>
agent commands:
herdr agent list
herdr agent get <target>
herdr agent read <target> [--source visible|recent|recent-unwrapped] [--lines N] [--format text|ansi] [--ansi]
herdr agent send <target> <text>
herdr agent rename <target> <name>|--clear
herdr agent focus <target>
herdr agent wait <target> --status <idle|working|blocked|unknown> [--timeout MS]
herdr agent attach <target> [--takeover]
herdr agent start <name> [--cwd PATH] [--workspace ID] [--tab ID] [--split right|down] [--focus|--no-focus] -- <argv...>
agent targets accept terminal ids, unique agent names, detected/reported agent labels, and legacy pane ids. agent list shows terminals with explicit agent identity, detected/reported agent identity, or agent.start launch metadata. pane.rename does not make a plain terminal appear in agent list; use agent.rename for agent identity.
terminal commands:
herdr terminal attach <terminal_id> [--takeover]
terminal attach is the low-level direct attach primitive. it streams the current rendered terminal state first, then live ANSI frames. direct attach has one writable input/resize owner; a second direct attach fails unless you pass --takeover. detach with ctrl+b q; send a literal ctrl+b with ctrl+b ctrl+b.
pane commands:
herdr pane list [--workspace <workspace_id>]
herdr pane get <pane_id>
herdr pane rename <pane_id> <label>|--clear
herdr pane read <pane_id> [--source visible|recent|recent-unwrapped] [--lines N] [--format text|ansi] [--ansi]
herdr pane split <pane_id> --direction right|down [--cwd PATH] [--focus] [--no-focus]
herdr pane close <pane_id>
herdr pane send-text <pane_id> <text>
herdr pane send-keys <pane_id> <key> [key ...]
herdr pane report-agent <pane_id> --source ID --agent LABEL --state idle|working|blocked|unknown [--message TEXT] [--custom-status TEXT] [--seq N]
herdr pane run <pane_id> <command>
wait commands:
herdr wait output <pane_id> --match <text> [--source visible|recent|recent-unwrapped] [--lines N] [--timeout MS] [--regex] [--raw]
herdr wait agent-status <pane_id> --status <idle|working|blocked|done|unknown> [--timeout MS]
cli behavior notes
statusprints local client version/protocol, running server version/protocol when reachable, socket path, compatibility, and whether a restart is neededstatus serverprints only the running server side; if no server is reachable it exits successfully and printsstatus: not runningstatus clientprints only the local executable version/protocol and binary path without contacting the serverworkspace createkeeps focus where it is by default; pass--focusto switch to the new workspaceworkspace createwithout--labelkeeps the default cwd-based workspace namingworkspace create --labelapplies the custom workspace name immediatelyworkspace createreturnsresult.workspace,result.tab, andresult.root_panetab createkeeps focus where it is by default; pass--focusto switch to the new tabtab createwithout--labelkeeps the default numbered tab namingtab create --labelapplies the custom tab name immediatelytab createreturnsresult.tabandresult.root_paneagent listshows agent terminals, not arbitrary named panesagent wait --status idleis the CLI completion wait; it treats the UI-onlydoneattention state as satisfying idleagent focusswitches the TUI workspace/tab/pane focus; it is not direct terminal attachagent attachattaches the current terminal directly to the agent terminal streamagent start <name> -- <argv...>starts a named agent terminal and returnsresult.agentplusresult.argvpane splitkeeps focus where it is by default; pass--focusto switch to the new panepane readprints text, not jsonpane read --format ansiandpane read --ansiprint a rendered ANSI snapshot with colors/styles preservedpane read --source recent-unwrappedreturns recent terminal text with soft wraps joined back togetherpane send-text,pane send-keys,pane report-agent, andpane runprint nothing on success- list/get/create/split/wait commands print json on success
pane runis a convenience wrapper forpane.send_inputwith the command text followed by a realEnterkeypresswait agent-statusis a cli convenience built on top of event subscriptions- use it when you want the same
done/idledistinction the UI shows --rawis a legacy alias for ANSI formattedpane readoutput and still disables ansi stripping forwait outputwait output --source recentmatches against unwrapped recent terminal text by default, so pane width and soft wrapping do not break matches
cli examples
create a workspace, split a pane, run a server, and wait for readiness:
herdr workspace create --cwd /path/to/project --label "api server"
herdr pane split 1-1 --direction right --no-focus
herdr pane run 1-2 "npm run dev"
herdr wait output 1-2 --match "ready" --timeout 30000
wait for an agent by name to become idle for CLI automation:
herdr agent wait reviewer --status idle --timeout 60000
wait for a pane-level UI attention state:
herdr wait agent-status 1-1 --status done --timeout 60000
inspect another pane's output:
herdr pane read 1-1 --source recent --lines 80
behavior notes and gotchas
pane.send_textsends literal text only. if you want to execute a command, follow it withpane.send_keysandEnter, usepane.send_inputfor orderedtext + keypressinput, or use clipane run, which sends the text and then a real Enter key in one request.pane.readandpane.wait_for_outputstrip ansi by default.pane.output_matchedsubscriptions fire on transitions into a matching state; they do not repeatedly spam the same still-visible match on every poll.- closing the socket connection ends the subscription.
- there is no separate event transport.
- the same herdr process can serve regular request/response calls and long-lived subscription connections at the same time.