Herdr
A terminal multiplexer built for coding agents. It organises terminals into
workspaces, tabs and panes, recognises the agent running inside a pane, and exposes
the live session through the herdr CLI — that last part is the real difference from
tmux: an agent can open its own pane, dispatch a command, and read the output back.
Website:
Install#
macOS / Linux:
Windows:
The binary lands in ~/.local/bin/herdr. Note that a non-login shell usually does
not have that directory on PATH — command -v herdr over ssh will come back empty
even when it is installed. Don't read that as "not installed":
Updates and channels:
Config#
~/.config/herdr/config.toml:
The same directory holds session.json (persisted layout), herdr.sock (API socket)
and herdr-server.log.
Point it at pwsh on Windows#
A pane's shell defaults to $SHELL, which on Windows lands on the built-in Windows
PowerShell 5.1 rather than PowerShell 7. To get 7 inside panes you have to say so:
The documented behaviour is "when unset or empty, Herdr uses $SHELL, then /bin/sh on
Unix and PowerShell on Windows" — and that Windows fallback is the built-in 5.1. The
value is an executable name or path, not a shell command line.
Run herdr server reload-config afterwards, or just open a new pane. To check what
you're actually in:
Install 7 first if needed: winget install --id Microsoft.PowerShell. Note also that
5.1 and 7 have separate $PROFILE files (WindowsPowerShell\ vs PowerShell\), so
anything configured in the old one does not carry over.
The other two [terminal] options#
shell_mode — "auto" (default) / "login" / "non_login", controlling whether a new
pane's shell starts as a login shell. The documentation spells out the reason: "auto"
starts login shells on macOS so login-only PATH setup runs in new panes — things like
/usr/libexec/path_helper and Homebrew's shell initialisation.
Worth remembering: on macOS path_helper reorders the system paths to the front, so "the
PATH inside a pane differs from the one in my terminal" usually comes down to shell_mode.
new_cwd — "follow" (default) / "home" / "current" / a fixed path such as
"~/Projects". "follow" inherits the source pane or workspace; with no source, Herdr
starts in $HOME.
Validate with herdr's own checker rather than by eye:
CLI#
Running bare herdr launches or attaches the TUI, so don't use it to explore
commands. Print a command group instead:
Most commands return JSON. Read pane / tab / workspace ids out of the response rather than guessing them.
Running a command in a pane#
Herdr injects the caller's context into every managed pane:
HERDR_ENV=1 means you are currently inside a herdr pane.
Three gotchas found the hard way#
The pane is an interactive TTY, so pagers kick in#
git log, git diff, systemctl status and friends drop into less and sit there.
The trailing && echo DONE never runs, so wait-output just times out.
Completion markers must be unique per invocation#
wait-output searches the existing snapshot immediately, so a fixed marker matches
leftover output from the previous command and reports a hit straight away. That is
worse than a timeout: a timeout at least raises an error, a false positive convinces
you the command finished.
There is also no exit code coming back from a pane — success and failure have to be printed by the command itself.
Read output with --source visible#
recent / recent-unwrapped frequently come back with zero bytes. Don't use them to
decide whether a command produced output:
Running it under systemd (servers)#
Herdr's session.json restores the layout, cwds and pane labels — but not the
commands that were running in those panes; what comes back is a clean shell. So
autostart needs two layers: one to bring up the server, one to launch the services
into their panes.
/etc/systemd/system/herdr.service:
herdr server is described upstream as the headless server; it needs no TTY.
Then a oneshot unit that launches the services once the server is up. Locate panes by label, not by pane id — ids change across restarts, labels don't:
With Requires=herdr.service + After=herdr.service, restarting herdr drags this unit
along with it.
Using herdr on Windows over SSH#
Two limitations, both tied to the Windows version, and they pull in opposite directions — no single version is good for both.
Mouse input requires a Win11 host#
When clicking and scrolling do nothing over SSH, it is not that herdr failed to enable mouse capture — Windows 10's ConPTY drops the mouse reports before herdr can read them. ConPTY's mouse event translation only exists on Windows 11; it was never backported to 10, and the corresponding Microsoft Terminal issue is marked can't fix.
Nothing in the configuration works around this. It comes down to the host build:
But Win11 cannot traverse junctions#
Conversely, Windows 11 24H2+ tightened reparse point traversal, so an SSH session cannot see through a junction:
And herdr's install directory is exactly that — a junction:
The tell is that the link shows fewer files than its target:
What matters is not the path but who created the junction — the ones an installer creates
cannot be traversed, the ones mklink /J creates can. (Print name length looked like the
discriminator at first, but that was disproved: a mklink /J rebuild had a print name length
of 62 and still traversed.)
Rebuilding fixes it; rmdir removes only the link, never the target:
This recurs on every herdr upgrade — the target path carries the version number, so a new
release always means a new directory, and the junction the installer rebuilds is the kind that
cannot be traversed. vite-plus's current follows the same pattern.
House rules#
- Only close panes / tabs you created yourself; leave the user's alone
- Always
--no-focusfor background work - Sequential commands reuse one pane. When you genuinely need parallelism, split down from the right-hand pane and cap the column at 3-4. Splitting right over and over just keeps halving the width
- Never
herdr server stopfrom an active session — it takes the pane processes with it