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
- The Mental Model
- Install
- Core Navigation
- Workspaces, Tabs and Panes
- Git Worktrees
- Hosting Agents
- Driving Herdr From an Agent
- The Socket API and CLI
- Remote Sessions
- Sessions and the Server
- Configuration
- Troubleshooting
- Quick Reference
- Further Reading
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:
- The server is separate from the client. Panes keep running when you detach or close the terminal; reattaching re-connects to the same session.
- 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 theherdr <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:
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 inconfig.toml(prefix = "β¦"under[keys]) and runherdr 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 = falseunder[ui]if you would rather have the outer terminal handle normal clicks (for example, Cmd-clicking URLs); pane apps likelazygitorbtopstill receive mouse when they request it. - Detaching is safe.
prefix+qleaves 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-focusfor background work so the user's focus does not jump. - Pass an explicit
--cwd(or rely on[terminal] new_cwd, defaulting tofollow) 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 startrequires 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 returnsagent_not_readyimmediately but keeps the name usable forreadandsend-keys.- Pass native agent flags after
--:herdr agent start reviewer --kind codex --pane w4:p2 -- --model gpt-5. agent prompt --waitwaits for the first settledidle,doneorblockedstate. If a prompt from a non-working state produces no observed lifecycle change within five seconds, it returnsagent_prompt_stalledrather than hanging.agent promptrefuses 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
--untilonly for a state-specific wait, e.g. waiting for a busy agent to ask for input:
agent send-keystakes 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:
Two gates apply. First, the agent must actually be running inside a Herdr-managed pane:
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-focusfor 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 stopfrom 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 snapshotis the whole live state β agents, panes, layouts, focus β in one object. herdr api schemaprints the bundled schema, which is the authoritative list of fields for anything you script.herdr agent explaintells you why an agent was or was not detected in a pane; reach for it when a pane showsunknown.- 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 statusprints the path in use. [server] headless_cols/headless_rowsset 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.
Sidebar 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
- Herdr documentation
- Herdr agent guide β first-run setup for an agent working inside Herdr
- Herdr troubleshooting notes
- Related pages in this KB: Tmux β Terminal Multiplexer Cheat Sheet