This page is the detailed mechanics/reference companion to the high-level README. It documents Herd's supported local control surface.
Use the herd CLI for normal automation. The raw socket protocol is still available for low-level integration and for the MCP bridge.
Herd exposes a grouped CLI through the app binary itself:
- installed usage:
herd - repo-local usage:
bin/herd
On app startup Herd refreshes ~/.local/bin/herd so the installed app is available on PATH without sudo. Inside the repo, bin/herd wraps the Rust CLI directly.
Global flags:
--socket <path>overrides the socket path--agent-pid <pid>marks the call as agent-originated metadata; Herd-managed agents should always include it
Examples:
herd network list
herd network list shell
herd tile list
herd tile list agent
herd tile list work
herd tile get AbCdEf
herd tile move AbCdEf 1180 260
herd tile resize AbCdEf 760 520
herd message channel listAgent, channel, chatter, network, and work operations are session-private. They resolve against the caller's current tmux tab/session and do not expose cross-session registry data.
Public control APIs use Herd tile_id, not tmux pane ids. Tmux-backed tiles get short Herd-owned IDs such as AbCdEf; work tiles use work:<work_id>.
Send a direct message to another agent by agent_id:
herd --agent-pid "$PPID" message direct agent-1234 "Can you take #prd-7?"Broadcast to public chatter:
herd --agent-pid "$PPID" message public "I am picking up #prd-7 and syncing with @agent-1234" --mention agent-1234Send to a subscribed channel:
herd --agent-pid "$PPID" message channel '#prd-7' "Starting the socket refactor now"Send directly to the current session Root agent:
herd --agent-pid "$PPID" message root "Please inspect local work items and assign follow-up"
herd --agent-pid "$PPID" sudo "Please inspect local work items and assign follow-up"Send to the sender's local network:
herd --agent-pid "$PPID" message network "Need another pass on this local network"List channels or manage channel subscriptions:
herd message channel list
herd message channel subscribe agent-1234 '#prd-7'
herd message channel unsubscribe agent-1234 '#prd-7'Acknowledge a backend ping for the current or named agent:
export HERD_AGENT_ID=agent-1234
herd agent ack-ping
herd agent ack-ping agent-1234Shell operations:
herd tile create shell --x 180 --y 140 --width 640 --height 400 --parent-tile-id AbCdEf
herd shell send GhIjKl "pwd\n"
herd shell exec GhIjKl "claude --help"
herd shell read GhIjKl
herd shell role GhIjKl claudeBrowser operations:
herd tile create browser --parent-tile-id AbCdEf
herd tile create browser --browser-incognito true --parent-tile-id AbCdEf
herd tile create browser --browser-path extensions/browser/checkers/index.html --parent-tile-id AbCdEf
herd tile create browser --browser-path extensions/browser/game-boy/index.html --parent-tile-id AbCdEf
herd browser navigate MnOpQr https://example.com
herd browser load MnOpQr ./index.html
herd browser load MnOpQr extensions/browser/texas-holdem/index.html
herd tile destroy MnOpQrCommand-bar equivalents in the UI:
:sudo <message>sends a Root message asUser:dm <agent_id|AgentNumber|root> <message>sends a direct message asUser:cm <message>sends a public message asUser
If a message arrives through the Claude channel and you want your reply to be visible to Herd or other agents, answer through the Herd messaging interface. Plain assistant text in the local session does not publish a Herd message.
Work operations:
herd tile create work --title "Socket refactor follow-up"
herd --agent-pid "$PPID" network connect AbCdEf left work:work-s4-001 left
herd --agent-pid "$PPID" network disconnect AbCdEf left
herd --agent-pid "$PPID" work stage start work-s4-001
herd --agent-pid "$PPID" work stage complete work-s4-001
herd self infoWorker tile-event subscriptions for network-visible tiles use direction:action selectors:
herd --agent-pid "$PPID" network subscribe MnOpQr in:exec
herd --agent-pid "$PPID" network unsubscribe MnOpQr in:exec
herd --agent-pid "$PPID" network subscriptions MnOpQrRoot can manage session-wide tile-event subscriptions for any same-session tile:
herd tile subscribe MnOpQr both:extension_call agent-1234
herd tile unsubscribe MnOpQr both:extension_call agent-1234
herd tile subscriptions MnOpQr agent-1234Worker MCP exposes the message tools plus self_info, self_display_draw, self_led_control, self_display_status, network_list, network_get, network_subscribe, network_unsubscribe, network_subscription_list, and network_call. Root also gets the broader session-scoped tile and subscription controls.
Herd exposes a newline-delimited JSON protocol on /tmp/herd.sock by default. If HERD_RUNTIME_ID is set, the socket path becomes /tmp/herd-<runtime_id>.sock.
Socket commands follow category_command naming.
Normal control surfaces target Herd tile_id only. Tmux pane/window ids remain internal backing metadata and are not part of the public control API.
self_infoself_display_drawself_led_controlself_display_status
self_info requires sender context and returns the sender tile's native get payload. It does not apply sender-visible network filtering.
self_display_draw requires an agent sender and updates only that calling agent tile's display drawer. It accepts text, columns, and rows.
self_led_control requires sender tile context and updates only that calling tile's 8-LED chrome strip. It accepts either commands or pattern_name, and the resulting sequence loops until replaced.
self_display_status requires sender tile context and updates only that calling tile's single-line ANSI status strip. Long text scrolls automatically in the tile chrome.
tile_createtile_listtile_destroytile_renamenetwork_listnetwork_gettile_gettile_calltile_movetile_resizenetwork_connectnetwork_disconnect
tile_create accepts tile_type = shell | agent | browser | work, plus optional title, x, y, width, height, parent_session_id, and parent_tile_id. Browser creation also accepts optional browser_incognito / CLI --browser-incognito true to start the browser tile in incognito mode instead of the shared default profile, plus optional browser_path / CLI --browser-path <path> to immediately load a local page such as an existing browser extension.
tile_list returns every tile in the current session. network_list returns the sender tile's sender-visible local network. Both accept optional tile_type filter shell | agent | browser | work.
tile_destroy is the generic session-scoped destroy path for any tile type.
tile_rename is root-only and accepts tile_id and title. It works for shell, browser, agent, and work tiles and returns the updated tile object.
network_get is a worker-safe lookup by tile_id inside the sender's sender-visible local network. It returns the same tile object shape used by network_list.tiles, including message_api for the network-visible interface. On browser tiles, that message_api advertises the drive > screenshot formats for PNG image plus Braille, ASCII, ANSI, and layout-preserving text output. If the loaded page is a browser extension, the payload also includes details.extension, and message_api may expose extension_call with the extension's declared methods.
tile_get is a root-only lookup by tile_id in the current session. It returns the full tile object, including details for that tile type and the full message_api. On browser tiles, that interface also advertises the full drive > screenshot format set and any currently loaded browser-extension methods.
tile_move is root-only and accepts tile_id, x, and y. It updates the canvas position for the tile and returns the updated tile object.
tile_resize is root-only and accepts tile_id, width, and height. It updates the canvas size for the tile and returns the updated tile object.
network_subscribenetwork_unsubscribenetwork_subscription_listtile_subscribetile_unsubscribetile_subscription_list
Subscription selectors use direction:action syntax:
- directions:
in,out,both,* - actions: tile message names such as
exec,get,navigate, orextension_call
Worker network_* subscription commands only target network-visible tiles in the sender's connected component. Root tile_* subscription commands can target any same-session tile.
shell_input_sendshell_execshell_output_readshell_role_set
The shell instance commands target Herd tile_id. shell_exec submits <command> plus a trailing newline to the existing shell tile. It runs the command inside the current shell process and keeps the tile usable for later reads and writes.
browser_navigatebrowser_loadbrowser_drive
browser_navigate accepts tile_id and url, and returns the browser state payload with currentUrl.
browser_load accepts tile_id and a local path. Relative paths resolve from the Herd project root, and the file must exist.
browser_drive accepts tile_id, action, and optional args.
Supported action values:
click- requires
args.selector
- requires
select- requires
args.selector - requires
args.value
- requires
type- requires
args.selector - requires
args.text - accepts optional
args.clear
- requires
dom_query- requires
args.js - returns serialized data from the child browser page
- requires
eval- requires
args.js - evaluates arbitrary child-page JavaScript and returns serialized data when possible
- requires
screenshot- captures the current browser tile view
- accepts optional
args.formatofimage,braille,ascii,ansi, ortext - accepts optional
args.columnswhenargs.formatisbraille,ascii,ansi, ortext - returns
{ "mimeType": "image/png", "dataBase64": "<base64 png>" }by default on the socket/test-driver surface - returns
{ "format": "braille", "text": "<braille>", "columns": 80, "rows": 24 }whenargs.formatisbraille - returns
{ "format": "ascii", "text": "<ascii>", "columns": 80, "rows": 24 }whenargs.formatisascii - returns
{ "format": "ansi", "text": "<ansi escape text>", "columns": 80, "rows": 24 }whenargs.formatisansi - returns
{ "format": "text", "text": "<layout-preserving text grid>", "columns": 80, "rows": 24 }whenargs.formatistext - on the MCP surface, screenshot-shaped results are emitted as actual image/text content instead of raw base64 JSON blobs
browser_drive targets the child browser webview directly. It does not use test_dom_query or test_dom_keys, which only operate on the main Herd UI webview.
Browser tiles loaded from extensions/browser/... may expose a discoverable extension API through:
globalThis.HerdBrowserExtension.manifest- requires
extension_id - requires
label - may declare
methods
- requires
globalThis.HerdBrowserExtension.call(method, args, caller)- must return synchronously
- receives caller context including
sender_tile_id, optionalsender_agent_id, optionalsender_agent_role,target_tile_id, andtarget_pane_id
When a page exposes that contract:
tile_get/network_getincludedetails.extension- the browser tile
responds_tolist includesextension_call message_apiexposes the extension methods as discoverable subcommands
Built-in extension-backed pages currently include local games and emulators such as Texas Hold'em, Game Boy, and JSNES.
agent_registeragent_unregisteragent_events_subscribeagent_ping_acknetwork_listtile_gettile_renametile_movetile_resizemessage_directmessage_publicmessage_channelmessage_networkmessage_rootmessage_channel_listmessage_channel_subscribemessage_channel_unsubscribesudoon the CLI is an alias that routes tomessage_root
Use tile_list with tile_type = agent for session-scoped agent discovery and network_list with tile_type = agent for connected-component agent discovery. Agent metadata is returned inside the tile details payload.
Permission boundary:
- Worker MCP tools expose the message surface plus:
self_infoself_display_drawself_led_controlself_display_statusnetwork_listnetwork_getnetwork_subscribenetwork_unsubscribenetwork_subscription_listnetwork_call
tile_get,tile_rename,tile_move, andtile_resizeare root-only.- Root MCP also exposes
browser_drive,tile_subscribe,tile_unsubscribe, andtile_subscription_list. - Worker
network_callis limited to visible local-network tiles and only to the worker-safe message subset for each tile kind.shelland directly controlledbrowsertiles expose write actions, includingextension_callwhen a loaded browser page advertises it;agentandroot_agenttiles stay read-only even when directly connected. - The raw socket is also used by the app, tests, and local CLI automation for session-scoped network/work actions.
- Direct work stage mutation is still gated by the derived owner connection.
Message-channel behavior:
- Herd delivers incoming agent traffic through
notifications/claude/channel. - Event metadata includes
from_agent_id,from_display_name,to_agent_id,to_display_name,channels,mentions,replay, andtimestamp_ms. replay=truemeans historical context, usually last-hour chatter replay, not a fresh request.replay=falsemeans live traffic.- Replies that should be seen by Herd or other agents must go back out through
message_direct,message_public,message_channel,message_network, ormessage_root.
Tile-event subscriptions deliver Claude channel events with kind = "tile_event".
Important metadata fields on those events include:
delivery_reasonsubscriptionimplicit_self_target
subscription_scopenetworktile
subscription_directioninoutboth
actionsubject_tile_idpeer_tile_idcaller_tile_idcaller_agent_idtarget_tile_idtarget_agent_idrpc_channeloutcomeargs_jsonresult_json
message_channel_listmessage_channel_subscribemessage_channel_unsubscribemessage_channel
Channels are normalized lowercase and always stored with a leading #. Channel list and subscription data are session-private. Subscribing to a missing channel creates it in the caller's current session. message_public remains session-wide chatter; message_channel is the subscription-gated path.
work_stage_startwork_stage_completework_review_approvework_review_improve
network_list returns the sender tile's connected component. tile_list returns every tile in the current session. Both accept optional tile_type filter shell | agent | browser | work and replace the old dedicated shell/agent/work list commands. They return:
session_idsender_tile_idonnetwork_listtilesconnections
Each tile entry includes common fields:
tile_idsession_idkindtitlexywidthheightwindow_idwhen the tile is backed by a tmux windowparent_window_idwhen the tmux window has tracked lineagecommandwhen the tile is backed by a tmux paneresponds_toexposing the generic tile operationsget,callplus the tile-specific message names for that kindmessage_apiexposing structured message metadata for the same visible operations, including args and browserdrivesubcommandsdetailswith type-specific metadata
network_list and network_get are port-aware for non-root callers:
- direct connection to the target tile's read/write port returns the full tile RPC surface in
responds_to, exceptagentandroot_agenttiles, which always stay read-only on the network - direct connection to a read-only target port returns only the read interface for that tile kind
- indirect visibility through the same connected component also returns only the read interface
- if traversal reaches another tile through that tile's gateway port, the tile is still visible but automatic traversal stops there
- the gateway tile itself can still see every segment directly attached to its own ports when it is the sender
message_api is filtered by the same access rules as responds_to.
Today the read interface is:
shell:get,call,output_readagent/root_agent:get,call,output_readbrowser:get,callwork:get,call
network_get is a worker-safe lookup by tile_id inside the sender's sender-visible local network. It returns the same tile object shape used by network_list.tiles. For actionable calls, inspect message_api for required args and browser drive subcommands instead of guessing nested payload shapes.
network_call is a worker-safe generic tile call by tile_id inside the sender's sender-visible local network. It accepts:
tile_idaction- optional
argsobject
network_call enforces the same port-aware access model used by network_list / network_get. A worker can only invoke message names exposed in that target tile's network-visible responds_to list for its current sender tile.
Agents should use message_direct, message_network, message_public, or message_root to coordinate with other agents. The network tile interface for agent and root_agent tiles is intentionally observational only.
self_info is the self-targeted path for getting the sender tile's own full get payload when network_get would otherwise return the network-visible projection.
Per-port settings are UI-driven in this pass:
- right-click a visible port to open
Access > Read | Read/WriteandNetworking > Broadcast | Gateway - red left indicator means the effective access mode is
read - orange right indicator means the effective networking mode is
gateway - changing access disconnects any live edge that would otherwise become
read -> read
tile_get is a root-only lookup by tile_id in the current session. It returns the full tile object, including details for that tile type.
tile_call is the root-level generic tile-message surface for any tile in the current session. It accepts:
tile_idaction- optional
argsobject
Current worker-safe messages are:
shellgetoutput_readinput_sendexec
agent/root_agentgetoutput_read
browsergetnavigateloaddriveextension_callwhen the loaded page advertisesHerdBrowserExtension
For shell tiles, exec submits <command> plus Enter to the existing pane. It does not respawn or replace the target shell process.
For generic network_call / tile_call, browser drive expects:
action:click|select|type|dom_query|eval|screenshot- optional nested
argsobject for that browser-drive action
For extension-backed browser tiles, extension_call expects:
method: string- optional
args: object
The available methods are declared by the loaded page and surfaced in details.extension.methods and message_api.
Browser tile message_api now spells this out directly:
navigateurl: string
loadpath: string
driveaction: "click" | "select" | "type" | "dom_query" | "eval" | "screenshot"- optional
args: object clickselector: string
selectselector: stringvalue: string
typeselector: stringtext: string- optional
clear: booleandefaulting totrue
dom_queryjs: string
evaljs: string
screenshot- optional
format: "image" | "braille" | "ascii" | "ansi" | "text" - optional
columns: number - returns
{ "mimeType": "image/png", "dataBase64": "<base64 png>" }by default - returns
{ "format": "braille", "text": "<braille>", "columns": 80, "rows": 24 }whenformatisbraille - returns
{ "format": "ascii", "text": "<ascii>", "columns": 80, "rows": 24 }whenformatisascii - returns
{ "format": "ansi", "text": "<ansi escape text>", "columns": 80, "rows": 24 }whenformatisansi - returns
{ "format": "text", "text": "<layout-preserving text grid>", "columns": 80, "rows": 24 }whenformatistext
- optional
extension_callmethod: string- optional
args: object - the declared methods appear as
subcommandsinmessage_api - extension method results are method-specific
- extension
screenshotmethods use the sameimage/braille/ascii/ansi/textpayload shapes asdrive > screenshot
Generic tile messages reject:
- tiles outside the sender's local network
- other sessions
- tile-specific action names not exposed for that tile kind
Every socket-backed command now passes through the same internal message-delivery layer except the dedicated streaming agent_events_subscribe path. Session-scoped list, create, destroy, registry, channel, network, and message operations route through the session receiver, tile-instance operations route through tile receivers, and test/debug commands route through the herd receiver. Herd records structured tile_message_logs entries with channel = cli | socket | mcp | internal, target metadata, wrapper command, message name, args, outcome, and timing.
tile_move is root-only and accepts tile_id, x, and y. It updates the canvas position for the tile and returns the updated tile object.
tile_resize is root-only and accepts tile_id, width, and height. It updates the canvas size for the tile and returns the updated tile object.
Work items are session-scoped. Use tile_list with tile_type = work for work discovery and tile_get for a single work tile payload. tile_get returns the common tile fields plus work-specific details. Tile creation for tile_type = work routes through the session receiver/message path, while work_stage_start, work_stage_complete, work_review_approve, and work_review_improve route through the work tile receiver/message path. Work items follow:
- stages:
plan -> prd -> artifact - statuses:
ready -> in_progress -> completed -> approved
Each work item auto-creates work channel #<work_id> and SQLite-backed stage content for:
planprdartifact
There is no separate persisted work/ document tree anymore; stage content lives in SQLite with the rest of the session state.
Only the owner may perform Herd-managed work updates. work_review_approve and work_review_improve are intended for the user-facing review flow.
test_drivertest_dom_querytest_dom_keys
Low-level example with the raw socket:
export HERD_SOCK=/tmp/herd.sock
export HERD_TILE_ID=AbCdEf
printf '%s\n' '{"command":"tile_list","sender_tile_id":"AbCdEf","tile_type":"agent"}' \
| socat - UNIX-CONNECT:$HERD_SOCKHerd-managed agent launches use:
claude --teammate-mode tmux --dangerously-load-development-channels server:herdEach launch gets:
HERD_AGENT_IDHERD_SOCK- tile/session context such as
HERD_TILE_ID
The checked-in Herd MCP server is also the agent channel server. When it sees HERD_AGENT_ID, it:
- registers the agent with Herd
- subscribes to agent events over the Herd socket
- forwards backend events to Claude through
notifications/claude/channel - acknowledges Herd
PINGevents so the backend can track liveness
Herd persists chatter/debug history in SQLite alongside the rest of the runtime registry state.
Every session also has one Root agent with stable id root:<session_id>. Root and worker agents both launch against the same checked-in server:herd entry; the MCP server switches between message-only worker mode and full-tool root mode by inspecting HERD_AGENT_ROLE and HERD_AGENT_ID.
The Root agent is visible in red on the canvas. If you close it through the UI confirmation flow, Herd immediately recreates it for that session.
The typed test_driver API is the supported UI automation surface for integration tests. It is available in debug builds and can also be enabled with HERD_ENABLE_TEST_DRIVER=1.
Example:
printf '%s\n' '{"command":"test_driver","request":{"type":"ping"}}' \
| socat - UNIX-CONNECT:$HERD_SOCKThe current request surface includes:
- Readiness and status:
ping,wait_for_ready,wait_for_bootstrap,wait_for_idle,get_status - State snapshots:
get_state_tree,get_projection - Keyboard and command bar control:
press_keys,command_bar_open,command_bar_set_text,command_bar_submit,command_bar_cancel - Toolbar and sidebar control:
toolbar_select_tab,toolbar_add_tab,toolbar_spawn_shell,toolbar_spawn_agent,toolbar_spawn_work,sidebar_open,sidebar_close,settings_sidebar_open,settings_sidebar_close,sidebar_select_item,sidebar_move_selection,sidebar_begin_rename - Tile and canvas control:
tile_select,tile_close,tile_drag,tile_resize,tile_title_double_click,canvas_pan,canvas_context_menu,canvas_zoom_at,canvas_wheel,canvas_fit_all,canvas_reset,tile_context_menu,port_context_menu,context_menu_select,context_menu_dismissThese tile-oriented requests take Herdtile_id, not tmux pane ids.tile_selectalso accepts optionalshift_key: truefor multi-select, and the context-menu requests cover batch lock/unlock, batch close, and per-port access/networking changes. - Close-confirm flow:
confirm_close_tab,cancel_close_tab
Current integration coverage also exercises settings-sidebar saved-session save/load/delete flows and the toolbar OPEN SESSION restore flow, using the typed requests above plus test_dom_query where there is no dedicated request type for a specific button or select control.
The projection now includes debug and agent state such as:
debug_tabagentschannelschatterconnections- per-tile port/network-derived state used by the canvas and activity views
For programmatic examples, see tests/integration/client.ts.
test_dom_query and test_dom_keys are still available behind the same gate, but they are manual debugging helpers rather than the supported automated integration surface.