herdr/SOCKET_API.md

24 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 skillSKILL.md. best when an agent inside herdr just needs to learn the workflow quickly.
  • cli wrappersherdr server stop, herdr workspace ..., herdr tab ..., 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:

  1. explicit herdr --session <name>: $XDG_CONFIG_HOME/herdr/sessions/<name>/herdr.sock or $HOME/.config/herdr/sessions/<name>/herdr.sock
  2. HERDR_SOCKET_PATH
  3. HERDR_SESSION=<name>: $XDG_CONFIG_HOME/herdr/sessions/<name>/herdr.sock or $HOME/.config/herdr/sessions/<name>/herdr.sock
  4. default session path: $XDG_CONFIG_HOME/herdr/herdr.sock or $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:

  • w64e95948145ed1
  • w64e95948146a82

pane ids are workspace-scoped and stable across workspace reorder:

  • w64e95948145ed1-1
  • w64e95948145ed1-2
  • w64e95948146a82-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-2 rather than becoming workspace-tab-pane triples

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",
  "workspace_id": "w64e95948145ed1",
  "tab_id": "w64e95948145ed1:1",
  "focused": true,
  "cwd": "/home/can/Projects/herdr",
  "label": "reviewer",
  "agent": "pi",
  "agent_status": "working",
  "revision": 0
}

label is an optional manual pane name set through pane.rename.

agent is an optional display label string.

  • when herdr detects a built-in agent, this is that built-in name like pi or claude
  • when a hook or plugin reports a custom agent through pane.report_agent, this can be any non-empty label like hermes
  • when no agent identity is known, it is omitted

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:

  • idle
  • working
  • blocked
  • done
  • unknown

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
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:

  • cwd is optional
  • if cwd is omitted, herdr uses its current working directory and falls back to / if needed
  • focus is optional in raw socket requests and defaults to false
  • the cli wrapper also defaults to no focus; pass --focus to 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_id is optional and defaults to the active workspace
  • cwd is optional; if omitted, herdr uses the focused pane cwd in that workspace when available
  • focus is optional in raw socket requests and defaults to false
  • the cli wrapper also defaults to no focus; pass --focus to 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:

  • source is required and must be visible, recent, or recent_unwrapped
  • lines is optional
  • current implementation defaults to 80 lines when lines is omitted and caps reads at 1000
  • format defaults to text; use ansi for a rendered VT/ANSI snapshot with styles preserved
  • strip_ansi defaults to true and is kept for compatibility

source meanings:

  • visible — current viewport
  • recent — recent scrollback text
  • recent_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:

  • direction must be right or down
  • cwd is optional
  • focus is optional in raw socket requests and defaults to false
  • the cli wrapper also defaults to no focus; pass --focus to 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"
}

notes:

  • source is required and identifies the reporting integration instance
  • agent is required and may be any non-empty label string
  • built-in names like pi are normalized to their public label form
  • custom labels like hermes are accepted as-is
  • while this authority is active, the reported agent and state override heuristic display for that pane
  • process detection still owns pane liveness and fallback when hook authority is cleared or released
  • message is optional metadata for the reporting integration

returns ok.

pane.clear_agent_authority

params:

{
  "pane_id": "1-1",
  "source": "custom:hermes"
}

notes:

  • source is optional
  • when source is omitted, any hook authority for that pane is cleared
  • when source is 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:

  • agent uses the same non-empty label rules as pane.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:

  • source must be visible, recent, or recent_unwrapped
  • lines is optional
  • timeout_ms is optional
  • strip_ansi defaults to true
  • for source = "recent", output matching uses unwrapped recent terminal text so soft wraps do not break matches
  • source = "recent_unwrapped" is also available on pane.read when 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.created
  • workspace.closed
  • workspace.focused
  • tab.created
  • tab.closed
  • tab.focused
  • tab.renamed
  • pane.created
  • pane.closed
  • pane.focused
  • pane.exited
  • pane.agent_detected

parameterized subscriptions:

  • pane.output_matched
  • pane.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 event value uses snake_case with underscores:
    • subscribe with workspace.created
    • receive workspace_created
  • when you subscribe to a parameterized subscription, the pushed event value keeps the dotted name:
    • subscribe with pane.output_matched
    • receive pane.output_matched

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_matched supports source, optional lines, matcher config, and optional strip_ansi
  • pane.agent_status_changed accepts an optional agent_status filter; 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"
  }
}

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.

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>

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 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

  • status prints local client version/protocol, running server version/protocol when reachable, socket path, compatibility, and whether a restart is needed
  • status server prints only the running server side; if no server is reachable it exits successfully and prints status: not running
  • status client prints only the local executable version/protocol and binary path without contacting the server
  • workspace create keeps focus where it is by default; pass --focus to switch to the new workspace
  • workspace create without --label keeps the default cwd-based workspace naming
  • workspace create --label applies the custom workspace name immediately
  • workspace create returns result.workspace, result.tab, and result.root_pane
  • tab create keeps focus where it is by default; pass --focus to switch to the new tab
  • tab create without --label keeps the default numbered tab naming
  • tab create --label applies the custom tab name immediately
  • tab create returns result.tab and result.root_pane
  • pane split keeps focus where it is by default; pass --focus to switch to the new pane
  • pane read prints text, not json
  • pane read --format ansi and pane read --ansi print a rendered ANSI snapshot with colors/styles preserved
  • pane read --source recent-unwrapped returns recent terminal text with soft wraps joined back together
  • pane send-text, pane send-keys, and pane run print nothing on success
  • list/get/create/split/wait commands print json on success
  • pane run is a convenience wrapper for pane.send_input with the command text followed by a real Enter keypress
  • wait agent-status is a cli convenience built on top of event subscriptions
  • use it when you want the same done / idle distinction the UI shows
  • --raw is a legacy alias for ANSI formatted pane read output and still disables ansi stripping for wait output
  • wait output --source recent matches 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 another agent to finish in the same user-facing sense the UI shows:

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_text sends literal text only. if you want to execute a command, follow it with pane.send_keys and Enter, use pane.send_input for ordered text + keypress input, or use cli pane run, which sends the text and then a real Enter key in one request.
  • pane.read and pane.wait_for_output strip ansi by default.
  • pane.output_matched subscriptions 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.