Skip to content

Herdr β€” Terminal Workspace Manager for AI Coding Agents

Last reviewed: 2026-09-17

Purpose: A working guide to Herdr β€” the pane/workspace runtime that hosts coding-agent CLIs, the keybindings and CLI you actually use, agent lifecycle handling, the socket API, remote sessions, and configuration.

Contents


Overview

Herdr is a terminal workspace manager built for AI coding agents. It is a single Rust binary β€” no Electron, no runtime to install β€” that organises terminals into workspaces β†’ tabs β†’ panes, recognises the coding agent running inside each pane, and exposes the live session over a local socket API.

It does not wrap, reimplement or replace the agents. It hosts the CLIs you already use β€” Claude Code, Codex, OpenCode, Gemini, Cursor, Copilot, Kimi, Qwen, Grok, Hermes and others β€” and adds the layer they are missing:

Without a runtime With Herdr
A terminal per task, lost when the window closes Persistent sessions that survive client disconnects
"Is that agent still working?" β€” by squinting at output Lifecycle states per pane: idle, working, blocked, done, unknown
A script can only send-keys blindly A typed socket API with JSON responses and stable IDs
Parallel agents in ad-hoc windows Named workspaces, tabs and panes, with a sidebar of agent status
Re-invented Git worktree juggling per tool Built-in worktree-backed workspaces

Two design choices are worth knowing up front, because everything else follows from them:

  1. The server is separate from the client. Panes keep running when you detach or close the terminal; reattaching re-connects to the same session.
  2. Mouse-first UI. Herdr captures mouse input by design, so the wheel and clicks belong to Herdr while it is focused. That is not a broken terminal scrollback.

[!NOTE] Because Herdr is a full-screen TUI, never launch it bare from a script or a non-interactive tool call β€” it will block. Use herdr status, herdr server, or the herdr <group> <subcommand> CLI surface.

Commands, keybindings and defaults below were verified against herdr 0.8.2 on the stable channel.


The Mental Model

Four primitives, and one distinction that trips people up.

Primitive What it is
Workspace A top-level container β€” one per project, repo or context. Owns tabs and a working directory.
Tab A view inside a workspace, like a browser tab. Owns a pane layout.
Pane One terminal. A real shell, running a command, a test, a server β€” or an agent.
Agent A recognised coding agent occupying a pane. Not the pane itself.

That last distinction matters: a pane exists whether or not it contains an agent, and the two are controlled by two different command groups.

  • herdr pane … controls raw terminals: split, resize, send text, read output, run commands.
  • herdr agent … controls the recognised agent in a pane: prompt it, wait for a state, read its output, validate its identity.

IDs and caller context

IDs are opaque, stable handles:

Object Shape Example
Workspace w<N> w1
Tab w<N>:t<N> w1:t1
Pane w<N>:p<N> w1:p1

Closed tab and pane IDs are never reused. A pane moved into another workspace keeps its process but receives a new workspace-qualified ID β€” the move response reports the old value as previous_pane_id, and only the moved process's inherited caller context still resolves it, so it is not a safe general target.

Herdr injects the calling pane's context as environment variables, which is how a tool inside a pane addresses itself rather than whatever the user happens to be looking at:

printf '%s\n' "$HERDR_WORKSPACE_ID" "$HERDR_TAB_ID" "$HERDR_PANE_ID"

Prefer --current when a command should target the calling pane. Omitting a target may act on the UI-focused pane, which can belong to another client β€” or to the user.


Install

Linux and macOS

The upstream installer is a short script that detects your platform, downloads the matching asset and verifies its SHA-256 before installing. Read it before running it β€” that is good hygiene for any install script:

curl -fsSL https://herdr.dev/install.sh -o /tmp/herdr_install.sh
sed -n '1,150p' /tmp/herdr_install.sh          # platform detect, manifest fetch, sha256, mv
sh /tmp/herdr_install.sh                       # installs to ~/.local/bin/herdr

What it does and does not do:

  • Installs one binary to ${HERDR_INSTALL_DIR:-$HOME/.local/bin} and marks it executable.
  • Refuses to install on Android/Termux, pointing you at a host to SSH into instead.
  • Does not edit your shell rc files. It only warns that the install directory is not on PATH.

So fix PATH in both directions β€” they solve different shells:

ln -sfn "$HOME/.local/bin/herdr" /usr/local/bin/herdr   # non-interactive shells, scripts, agents
export PATH="$HOME/.local/bin:$PATH"                     # interactive shells (~/.bashrc)

Package managers work too: brew install herdr, mise use -g herdr, or the Nix flake. On Windows, take the release zip and keep herdr.exe inside the extracted directory β€” it ships an app-local ConPTY runtime that will not travel with a copied executable.

After installing

herdr --version          # client version, channel, protocol
herdr status             # client + server state, socket path, update state
herdr update             # installer-managed installs only (brew/mise/Nix update themselves)
herdr channel set preview   # opt into the preview channel; `herdr channel show` to check

herdr status is the first thing to run when anything looks wrong β€” it reports the client protocol, whether the server is running, whether the two are compatible, and whether a restart is pending.


Core Navigation

Everything hangs off a prefix key, ctrl+b by default. Press it, release, then press the action key.

Keys Action
prefix+? Keybinding help
prefix+s Settings
prefix+q Detach (server and panes keep running)
prefix+shift+r Reload config
prefix+w / prefix+g Workspace picker / goto
prefix+b Toggle the sidebar
prefix+o Open the notification target

The sidebar is the cockpit: it lists workspaces and their agents with a state indicator per row, so you can see at a glance which agent is working, which is blocked waiting for approval, and which finished while you were elsewhere.

Three things to know:

  • The default prefix collides with tmux. They are both ctrl+b. If you nest one inside the other, you must send the prefix twice to reach the inner one. Change it in config.toml (prefix = "…" under [keys]) and run herdr server reload-config.
  • Mouse input belongs to Herdr while focused β€” wheel, clicks and drag-selection are handled by Herdr's own UI. Set mouse_capture = false under [ui] if you would rather have the outer terminal handle normal clicks (for example, Cmd-clicking URLs); pane apps like lazygit or btop still receive mouse when they request it.
  • Detaching is safe. prefix+q leaves the server and every pane process alive. That is the feature, not a leak β€” but it also means a long-running agent keeps working after you close the terminal.

Workspaces, Tabs and Panes

Keybindings

Keys Action
prefix+shift+n / prefix+shift+w / prefix+shift+d New / rename / close workspace
prefix+c / prefix+shift+t / prefix+shift+x New / rename / close tab
prefix+p / prefix+n Previous / next tab
prefix+1…9 Switch to tab by index
prefix+v / prefix+minus Split pane vertically / horizontally
prefix+h j k l Focus pane left, down, up, right
prefix+tab / prefix+shift+tab Cycle panes
prefix+z Zoom (fullscreen) the pane
prefix+r Resize mode
prefix+x / prefix+shift+p Close / rename pane
prefix+e Edit pane scrollback in your editor

From the CLI

The same operations are available as commands returning JSON, which is what makes Herdr scriptable:

herdr workspace list
herdr workspace create --cwd ~/projects/api
herdr tab create --workspace "$HERDR_WORKSPACE_ID" --name logs
herdr pane split --current --direction right --cwd "$PWD" --no-focus
herdr pane list --workspace "$HERDR_WORKSPACE_ID"
herdr pane layout --pane "$HERDR_PANE_ID"

Creation responses hand you the IDs to use next β€” workspace create returns .result.workspace, .result.tab and .result.root_pane; pane split returns the new pane at .result.pane. Parse IDs out of the JSON rather than guessing them from sidebar order.

Practical layout habits:

  • Split a wide pane to the right, a narrow or tall pane down. Repeated same-direction splits produce unusably thin columns.
  • Pass --no-focus for background work so the user's focus does not jump.
  • Pass an explicit --cwd (or rely on [terminal] new_cwd, defaulting to follow) instead of assuming an inherited directory.

Git Worktrees

Herdr can back a workspace with a Git worktree, which is the clean way to run two agents on the same repository without them fighting over one checkout.

herdr worktree list
herdr worktree create feature/login --repo ~/projects/api
herdr worktree open ~/.herdr/worktrees/api/feature-login
herdr worktree remove feature-login

Worktrees live under ~/.herdr/worktrees by default; override with [worktrees] directory in the config. Keybinding prefix+shift+g creates a worktree workspace interactively.

Why it matters for agent work: each worktree has its own branch and working tree, so a refactor agent and a test-writing agent cannot corrupt each other's uncommitted changes, and git status in either pane means exactly one thing.


Hosting Agents

Wire an agent in once

Herdr detects agents by process inspection, but an integration makes the pane report the agent's real lifecycle state instead of inferring it.

herdr integration list                 # available integrations
herdr integration install claude
herdr integration status               # what is current, what is missing
herdr integration status --outdated-only
herdr integration uninstall claude     # clean and reversible

Integrations are per agent β€” installing one for Claude does not wire up Codex or OpenCode. herdr integration status lists every known agent and tells you which are installed, current, or outdated.

install writes into the agent's own configuration tree and enables it there. Treat a vendor installer editing its own agent's config as expected behaviour, and keep the changes reviewable.

Lifecycle states

This is the vocabulary the sidebar, the CLI and any supervising code share:

State Meaning
idle Ready for input, and its tab has been seen in the focused UI
working Actively running
blocked Herdr recognised an approval or question prompt waiting on a human
done Same underlying idle state, after unseen background work finished
unknown An agent is present but could not be classified confidently β€” this does not mean it finished

idle and done differ only by whether you have looked: focusing the tab (or targeting the pane/agent with a focus command) marks it seen; reading it through the CLI does not.

Start, prompt and read an agent

herdr pane split --current --direction right --cwd "$PWD" --no-focus
herdr agent start reviewer --kind codex --pane w4:p2
herdr agent prompt reviewer "Review the diff and report only actionable findings." --wait --timeout 120000
herdr agent get reviewer
herdr agent read reviewer --source recent-unwrapped --lines 120

Notes that save time:

  • agent start requires an existing pane sitting at an interactive shell prompt. It never creates or moves layout, and it returns only once the expected agent is detected and ready. Startup defaults to a 30-second timeout; if the agent blocks on a startup dialog it returns agent_not_ready immediately but keeps the name usable for read and send-keys.
  • Pass native agent flags after --: herdr agent start reviewer --kind codex --pane w4:p2 -- --model gpt-5.
  • agent prompt --wait waits for the first settled idle, done or blocked state. If a prompt from a non-working state produces no observed lifecycle change within five seconds, it returns agent_prompt_stalled rather than hanging.
  • agent prompt refuses to type into an agent already sitting at an approval dialog (agent_blocked). That is a safety feature β€” inspect the dialog and ask the user before answering it.
  • Use --until only for a state-specific wait, e.g. waiting for a busy agent to ask for input:
herdr agent wait reviewer --until blocked --timeout 120000
  • agent send-keys takes logical keys, validated before any bytes are written: herdr agent send-keys reviewer esc, herdr agent send-keys reviewer ctrl+c.
  • Agent targets accept a unique live agent name or the pane ID hosting it β€” not terminal IDs, not bare kind labels.

Run an ordinary command in another pane

herdr pane split --current --direction right --cwd "$PWD" --no-focus
herdr pane run w4:p3 "just test"
herdr pane wait-output w4:p3 --match "test result" --timeout 120000
herdr pane read w4:p3 --source recent-unwrapped --lines 120

pane run sends the command and Enter atomically. pane wait-output searches the selected snapshot immediately, so output that already exists can match; use --match for a literal substring or --regex for a Rust regular expression, and omit --timeout to wait indefinitely.


Driving Herdr From an Agent

Herdr ships an agent-facing skill so a coding agent can inspect and control neighbouring panes. Print it with:

herdr --skill

Two gates apply. First, the agent must actually be running inside a Herdr-managed pane:

test "${HERDR_ENV:-}" = 1

If that fails, the agent is outside Herdr and must not reach in to control the focused session. Second, the skill is only in scope when the task genuinely calls for pane control β€” not merely because a task could use a background terminal.

The rules the shipped skill asks an agent to follow are good defaults for any automation built on Herdr:

  • Default to a sibling pane in the current tab and the current directory; do not invent workspaces, tabs, worktrees or directories the user did not ask for.
  • Use --no-focus for background work, and --current (or an explicit ID/agent name) rather than relying on someone else's focused pane.
  • Read IDs from JSON responses rather than deriving them.
  • Do not close workspaces, tabs, panes or sessions you did not create.
  • Never run herdr server stop from inside an active session, and never kill the main Herdr process β€” use a named test session for experiments that need an isolated server.

Reading pane output

--source Returns
visible The currently rendered viewport
recent Recent rendered output, including soft wraps (default)
recent-unwrapped Recent output with soft wraps joined β€” prefer for logs and transcripts
detection The plain-text bottom-buffer snapshot used for agent detection

Use --format ansi when colour is itself the evidence; otherwise text. --lines asks for more rows from the screen plus host scrollback β€” and if raising it stops revealing more of a completed response, the agent is probably painting to the terminal's alternate screen, whose rows never enter scrollback. The fallback then is to ask the agent to write its full response to a file and read that file, rather than requesting file output in the original prompt.


The Socket API and CLI

The CLI group is the public surface over the local socket:

Group Purpose
herdr workspace <list\|create\|get\|focus\|rename\|close> Workspaces, plus report-metadata for display-only info
herdr tab <list\|create\|get\|focus\|rename\|close> Tabs
herdr pane <…> Raw terminals: split, swap, move, resize, zoom, read, input, send-text, send-keys, wait-output, run, layout, process-info, neighbor, edges
herdr agent <…> Recognised agents: start, prompt, wait, read, send-keys, get, list, focus, rename, attach, explain
herdr worktree <…> Git-worktree workspaces
herdr session <list\|attach\|stop\|delete> Named persistent sessions
herdr integration <install\|uninstall\|status> Agent integrations
herdr notification show Surface a notification
herdr api <snapshot\|schema> Live session snapshot and the bundled API schema

Useful details:

  • Most control commands return JSON. herdr api snapshot is the whole live state β€” agents, panes, layouts, focus β€” in one object.
  • herdr api schema prints the bundled schema, which is the authoritative list of fields for anything you script.
  • herdr agent explain tells you why an agent was or was not detected in a pane; reach for it when a pane shows unknown.
  • CLI server errors are JSON on stderr with exit status 1; syntax errors exit with status 2. Branch on the exit code, and parse stderr when it is 1.

Remote Sessions

Herdr can run the session on a remote host and attach locally:

herdr --remote user@host
herdr --remote user@host --session work
herdr --remote user@host --remote-keybindings local   # or: server
herdr update --handoff

Under [remote] manage_ssh_config = true (the default), Herdr runs SSH through a generated config that includes your ~/.ssh/config first and then adds ServerAliveInterval / ServerAliveCountMax as fallbacks β€” so keepalive values you set yourself still win, and idle NAT timeouts do not silently kill the session. It also reuses the first authenticated connection via a private OpenSSH control socket. Set it to false to run plain SSH against your config untouched.

Because the panes live on the server side, detaching from a remote session does not stop the agents running there β€” which is exactly what you want for long jobs, and worth remembering before you walk away from a git reset you were supervising.


Sessions and the Server

herdr                       # launch, or attach to the default session
herdr --session work        # a named session
herdr session list
herdr session attach work
herdr session stop work
herdr session delete work   # only once stopped
herdr server stop           # stop the server and its panes
herdr server reload-config  # apply config.toml changes
  • The server outlives the client. Detaching or closing the terminal leaves the server, its workspaces and every pane process running.
  • Named sessions let you keep unrelated contexts apart β€” and give you an isolated server for experiments without touching your live work.
  • The socket lives at ~/.config/herdr/herdr.sock (per session). herdr status prints the path in use.
  • [server] headless_cols / headless_rows set the virtual terminal size used when no client is attached; an attached client always uses its own size.
  • Under [session] resume_agents_on_restore = true (the default), panes whose agent reports a session reference can be resumed into their native conversation after a server restart. That requires the official integrations, since only they report session references.

Configuration

Config lives at ~/.config/herdr/config.toml. Print the fully commented defaults to see every supported key:

herdr --default-config
herdr config check          # validate and get diagnostics
herdr config reset-keys     # backs up config.toml, then strips custom keybindings
herdr server reload-config  # apply changes without restarting

Highlights of what is worth tuning:

Section Keys worth knowing
[theme] One of eleven built-ins (catppuccin, tokyo-night, dracula, nord, gruvbox, one-dark, solarized, kanagawa, rose-pine, vesper, terminal); auto_switch to follow the host's light/dark appearance; [theme.custom] to override individual colour tokens
[terminal] default_shell (empty = $SHELL then /bin/sh), shell_mode = auto\|login\|non_login, new_cwd = follow\|home\|current\|<path>
[keys] prefix, every action binding, and [[keys.command]] for your own commands
[ui] sidebar_width, mouse_capture, copy_on_select, pane_borders, pane_gaps, hide_tab_bar_when_single_tab, window_title tokens ({hostname} {workspace} {tab} {pane}), agent_panel_sort = spaces\|priority
[ui.toast] Notification delivery: off, herdr (in-app), terminal (outer terminal), system (OS notification service)
[ui.sound] Sounds when background agents change state, with per-agent overrides and custom mp3 paths
[worktrees] Where worktree checkouts are created
[remote] manage_ssh_config
[session] resume_agents_on_restore
[experimental] allow_nested, kitty_graphics, pane_history, switch_ascii_input_source_in_prefix, reveal_hidden_cursor_for_cjk_ime
[advanced] scrollback_limit_bytes (default 10 MB per pane)

Custom commands

Three binding types, all under [keys]:

[[keys.command]]
key = "prefix+alt+g"        # type = "shell" runs detached; "pane" opens a temporary
type = "popup"              # pane that closes on exit; "popup" is a modal that
command = "lazygit"         # leaves the tab layout untouched
width = "80%"               # cells or percentages
height = "80%"

type = "shell" runs the command detached in the background, type = "pane" opens a temporary pane that closes when the command exits, and type = "popup" shows a session-modal terminal without disturbing the layout.

The sidebar's agent and space rows are configurable, with built-in tokens (state_icon, state_text, workspace, tab, pane, agent, branch, git_status, terminal_title) and $name tokens for metadata reported through the API:

[ui.sidebar.agents]
rows = [["state_icon", "workspace", "tab"], ["agent"]]

[ui.sidebar.spaces]
rows = [["state_icon", "workspace"], ["branch", "git_status"]]

Custom metadata can also be pushed from your own tooling with herdr workspace report-metadata and herdr pane report-metadata β€” a clean way to make the sidebar show something Herdr cannot infer, like CI state or Jira ticket IDs.


Troubleshooting

Symptom Cause and fix
command not found: herdr from a script or agent, but it works in your terminal Non-interactive shells do not read your rc file. Symlink the binary into /usr/local/bin (see Install)
Pane shows unknown instead of a state The agent is present but not classified. Run herdr agent explain, then install that agent's integration and re-check herdr integration status
agent start returns agent_not_ready The agent is blocked on a startup dialog. Read the pane, resolve it, then prompt once it is idle
agent prompt returns agent_blocked The agent is waiting at an approval or question prompt. Inspect it and ask the user β€” do not auto-answer
agent prompt returns agent_prompt_stalled No lifecycle change was observed within five seconds of prompting from a non-working state. Check agent get before re-sending
Raising --lines stops revealing more output The agent is painting to the alternate screen, whose rows never enter scrollback. Ask it to write its full response to a file instead
Nested multiplexer shortcuts do not register The tmux and Herdr prefixes both default to ctrl+b; send the prefix twice, or rebind prefix in config.toml
Mouse wheel/selection behaves oddly in the emulator Herdr captures mouse input by design while focused. Set mouse_capture = false, or use the emulator's escape hatch for mouse reporting
Server still running after you quit Expected β€” the server outlives the client. herdr server stop if you want a clean host
Config changes seem ignored herdr config check to validate, then herdr server reload-config. Some settings (e.g. sidebar start state) apply on next launch
CLI exits 1 or 2 with no visible message Exit 1 = server error with JSON on stderr; exit 2 = syntax error. Read stderr, and check herdr api schema for the expected fields

Quick Reference

Task Command
Version, channel, protocol herdr --version
Server/client/socket state herdr status
Update (installer-managed) herdr update [--handoff]
Update channel herdr channel show / herdr channel set stable\|preview
List live state herdr workspace list, herdr tab list, herdr pane list, herdr agent list
Whole snapshot as JSON herdr api snapshot
Split a pane without stealing focus herdr pane split --current --direction right --cwd "$PWD" --no-focus
Run a command in a pane herdr pane run <pane> "<command>"
Wait for matching output herdr pane wait-output <pane> --match "<text>" --timeout 120000
Start an agent in a shell pane herdr agent start <name> --kind <kind> --pane <pane>
Prompt and wait herdr agent prompt <name> "<text>" --wait --timeout 120000
Wait for a specific state herdr agent wait <name> --until blocked --timeout 120000
Read agent output herdr agent read <name> --source recent-unwrapped --lines 120
Wire an agent in herdr integration install <agent> / herdr integration status
Print default config herdr --default-config
Validate config / rebind keys herdr config check / herdr config reset-keys
Apply config herdr server reload-config
Named session herdr --session <name> / herdr session attach <name>
Remote session herdr --remote user@host
Stop the server herdr server stop
Agent skill file herdr --skill

Further Reading