Skip to content

herdr: The Agent Multiplexer With a Scriptable API

You have five agents running. One is mid-refactor, one finished eleven minutes ago, one has been sitting on a permission prompt since you went to get coffee, and two are doing something you’d have to read 200 lines of scrollback to reconstruct. In plain tmux you find that out by visiting each pane. The cost is not the keystrokes — it’s that the blocked agent burned eleven minutes of wall-clock doing nothing, and you had no way to know.

herdr is a terminal multiplexer that knows what a coding agent is. Same real PTY panes as tmux, same persistent server, but every pane is classified idle, working, or blocked and grouped in a sidebar. And underneath the TUI there is a socket API, which turns out to be the more interesting half: it is the primitive for an agent that spawns and supervises other agents.

  • A running herdr fleet, and the honest story about how its state detection works
  • Why installing your agent’s lifecycle hooks changes blocked from a heuristic into a fact
  • The herdr agent CLI: start, prompt, wait, read, send-keys — everything a script needs
  • A working orchestrator: one agent that fans out to workers, waits on them, and collects results
  • Where herdr stops, and what you still have to own (isolation, review, cost)

herdr is a single Rust binary, around 10MB, Apache-2.0 licensed. No Electron, no account, no telemetry — your code stays on your machine.

Terminal window
curl -fsSL https://herdr.dev/install.sh | sh
# or
brew install herdr
# or
mise use -g herdr

Linux and macOS are stable; Windows is a preview beta. Then just start it and run agents the way you already do:

Terminal window
herdr

The interaction model is mouse-first — right-click to split and switch — with ctrl+b as the keyboard prefix, so tmux muscle memory mostly transfers. The part that does not exist in tmux is the sidebar: every pane grouped by agent state, so “who needs me” is a glance rather than a tour.

This is the feature you are adopting herdr for, so it is worth knowing precisely what it does.

herdr classifies each pane as idle, working, or blocked, using one of two mechanisms depending on the agent:

  • Lifecycle hooks. Where the agent supports them, herdr installs hooks and the agent reports its own transitions. This is exact.
  • Screen manifests. Otherwise herdr reads the rendered terminal. The docs are refreshingly precise about the limit: herdr only marks a pane blocked when the live bottom-buffer snapshot matches a known visible approval, question, or permission UI.

Read that second bullet again, because it defines the failure mode. Screen-manifest detection recognizes known prompt shapes. A new permission dialog, a custom Notification your team added, an agent asking a free-text question in an unfamiliar format — none of those are guaranteed to register. The pane keeps reading as working while it waits for you.

Coverage differs by agent, and it is the single most useful table for planning a fleet:

Support levelAgents
Full lifecycle hooksPi, OMP, Kimi Code CLI, Hermes Agent, MastraCode
Partial lifecycleClaude Code, Codex, Cursor Agent CLI, GitHub Copilot CLI, Devin CLI, Droid, OpenCode, Kilo Code CLI, Qoder CLI
Screen manifest onlyAmp, Grok CLI, Antigravity CLI, Kiro CLI, Maki
Less thoroughly testedGemini CLI, Cline

The three agents this guide cares about sit in the partial tier, which is a good place to be but not the top one. Any terminal agent runs as a plain pane regardless — you just get less signal.

herdr runs a background server, so panes survive detaching, and you can reattach from anywhere:

  1. Detach and walk away. The agents keep going. This is the same guarantee tmux gives you, and it is the reason a terminal multiplexer beats a desktop app for long runs.

  2. Put the server where it can stay up. A small VPS or a spare machine means closing the lid stops nothing: ssh box -t herdr and the fleet is back.

  3. Check on it from your phone. The layout is mouse- and touch-responsive over SSH, which sounds like a gimmick until an agent blocks while you are out and you can unblock it from a train.

  4. Restore deliberately after a reboot. herdr’s layout export/apply brings back the pane structure. It does not bring back agent conversations — pair it with each agent’s own resume (claude --resume, codex resume).

This is where herdr separates from tmux. Instead of typing into panes and grepping their output, you address agents by name and wait on semantic states.

Terminal window
# start a named agent in a specific pane
herdr agent start reviewer --kind codex --pane "$review_pane"
# name an agent that is already running
herdr agent rename w1:p2 reviewer
# send a prompt and block until it finishes
herdr agent prompt reviewer "Review the current diff for missed error paths" \
--wait --timeout 120000
# block until the agent reaches a state
herdr agent wait reviewer --until blocked --timeout 120000
# read what it produced
herdr agent read reviewer --source recent-unwrapped --lines 120
# send UI keys (escape, arrows, control chords) rather than text
herdr agent send-keys reviewer esc

Agent names follow [a-z][a-z0-9_-]{0,31}, and --kind accepts the recognized agents — claude, codex, cursor, pi, gemini, devin, cline, and others. Anything after -- is passed to the agent’s own CLI, which is how you pin a model or a permission mode per worker.

Two details worth internalizing. --until takes lifecycle states (idle, done, blocked, unknown), so a script can distinguish “finished” from “needs a human” without parsing anything. And --source recent-unwrapped matters for the same reason capture-pane -J matters in tmux: unwrapped output keeps long file paths and stack frames on one line, so your grep actually matches.

For plain shell work there are pane-level equivalents:

Terminal window
herdr pane run w1:p3 "just test --watch"
herdr pane wait-output w1:p3 --regex "passed|failed" --timeout 120000
herdr pane read w1:p1 --source recent --lines 50

Because the CLI is scriptable and state-aware, an agent can drive herdr — which means one agent can build the fleet, prompt it, wait on it, and read the results. This is the thing herdr does that almost nothing else in this category exposes.

#!/usr/bin/env bash
# bin/fanout -- one worker per task, supervised, results collected
set -euo pipefail
TASKS=("api-rate-limit" "webhook-idempotency" "cache-invalidation")
BASE=$(herdr pane list --json | jq -r '.panes[0].pane_id')
WORKERS=()
# herdr agent names must match [a-z][a-z0-9_-]{0,31}. Derive the name ONCE and
# address the agent by it everywhere; the raw task slug is not a valid handle.
# The "w-" prefix guarantees the required leading letter.
agent_name() {
printf 'w-%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9_-' '-' | cut -c1-32
}
# 1. build the layout and start one worker per task
for task in "${TASKS[@]}"; do
name=$(agent_name "$task")
WORKERS+=("$name")
pane=$(herdr pane split "$BASE" --direction right --json | jq -r '.pane_id')
herdr agent start "$name" --kind claude --pane "$pane" -- --worktree "$task"
done
# 2. prompt every worker without blocking on any of them
for i in "${!TASKS[@]}"; do
herdr agent prompt "${WORKERS[$i]}" \
"You own exactly one task: ${TASKS[$i]}. Stay inside your worktree. When the
tests pass, commit on your branch and stop. Do not touch other areas."
done
# 3. supervise: classify every worker as done, blocked, or timed out
for i in "${!TASKS[@]}"; do
name="${WORKERS[$i]}"
if herdr agent wait "$name" --until done --timeout 1800000; then
echo "== ${TASKS[$i]} DONE =="
herdr agent read "$name" --source recent-unwrapped --lines 60
elif herdr agent wait "$name" --until blocked --timeout 1000; then
# already blocked, so this second wait returns immediately
echo "!! ${TASKS[$i]} BLOCKED -- waiting on a human"
herdr agent read "$name" --source recent-unwrapped --lines 30
else
echo "!! ${TASKS[$i]} TIMED OUT -- still working after 30 min"
herdr agent read "$name" --source recent-unwrapped --lines 30
fi
done

Note what the script does not do: it never creates a worktree. -- --worktree "$task" passes the flag through to Claude Code, which owns its own isolation. herdr owns panes and state; the agent owns the filesystem. Keeping that line clean is what stops the two layers fighting.

The other detail worth copying is the agent_name indirection. A task slug is not an agent handle — herdr names must match [a-z][a-z0-9_-]{0,31}, so anything with an uppercase letter, a dot, or a slash gets transformed on the way in. Derive the name once, keep it in a parallel array, and address every later prompt / wait / read by that name. Sanitize at start and then address by the raw slug and you get the worst possible failure: the worker runs happily under its sanitized name while your prompts and waits silently target an agent that does not exist.

The CLI is a client for a local socket API, and you can talk to it directly when you need event subscriptions or want to drive herdr from something other than bash.

The transport is newline-delimited JSON over a Unix domain socket (named pipes on Windows). One request per line, responses matched by request ID:

{"id":"req_1","method":"ping","params":{}}
{"id":"req_1","result":{"type":"pong"}}

Socket path resolution, in order: an explicit --session <name>, then HERDR_SOCKET_PATH, then HERDR_SESSION=<name>, then the default ~/.config/herdr/herdr.sock.

Methods are grouped by domain:

DomainOperations
workspacecreate, list, focus, rename, close
tabcreate, list, focus, rename, move, close
panesplit, swap, focus, resize, read, send input, close
agentlist, read, prompt, wait, start, focus
layoutexport, apply, set split ratios
eventssubscribe, wait for output changes
{"id":"req_1","method":"pane.split","params":{"pane_id":"w1:p1","direction":"right","ratio":0.333}}
{"id":"req_2","method":"pane.run","params":{"pane_id":"w1:p2","command":["npm","test"]}}

The events domain is the one worth reaching for: subscribe on a long-lived connection and you get state changes pushed to you instead of polling. That is the difference between an orchestrator that reacts in a second and one that reacts on its next 15-second tick.

Before you build against any of it, generate the contract rather than guessing:

Terminal window
herdr api schema --json

herdr versions the protocol, so a client should check server capabilities before relying on a new method, and tolerate unknown fields rather than failing on them.

--kind claude, and pass isolation through: -- --worktree <task> puts the worker in .claude/worktrees/<task>/ on its own branch. Interactive runs need workspace trust first, so run claude once in the repo by hand before scripting it.

Claude Code’s own hooks are what lift it from screen-manifest guessing to real signal, so install herdr’s hooks for it. If your team also uses hooks for other purposes, keep them additive — a Stop hook that exits non-zero will surface as a failure on every single stop.

You trusted working on an agent that was actually blocked. Screen-manifest detection only recognizes known prompt shapes. Install lifecycle hooks, and treat a worker that has been working with no output for a long time as suspicious rather than busy.

Your orchestrator hangs. It waited for done on a worker that went blocked. Always pass --timeout, always handle the non-zero exit, and print the worker’s recent output when you do.

prompt or wait targets an agent that does not exist. You sanitized the name at agent start but addressed the raw task slug afterwards. herdr names must match [a-z][a-z0-9_-]{0,31}, so derive the handle once and reuse it — the worker runs fine under its real name while your commands quietly go nowhere.

Two layers fought over worktrees. herdr does not manage isolation and should not be made to. Let the agent create its worktree via its own flag; herdr’s job stops at the pane.

Ports, .env, and the dev database collided anyway. Worktrees isolate files only. Six workers running npm run dev fight over port 3000 regardless of how clean your multiplexer is. Audit the repo first — there is a copy-paste audit prompt in alternative IDEs and agent shells — or use container-per-agent isolation instead.

You built against methods that do not exist. The socket API is versioned and evolving. Run herdr api schema --json and build from the output, not from an example in a blog post — including the examples in this article, which are accurate as of July 2026 and will drift.

Rate limits stopped the fleet, not herdr. Every pane spends the same account’s quota, and a worker that hit a limit often looks identical to one that is thinking. Check the pane before debugging the harness.

The fleet outran your review. Five workers producing five branches produce five reviews, all arriving at once. Accepted throughput is the number that matters — see team parallelism for calibrating the ceiling and cost optimization for the token side.

Windows behaved differently. Windows support is a preview beta and uses named pipes rather than Unix sockets. Treat scripted orchestration there as experimental.