You start a long agent task. You close the lid, or the wifi drops, and the terminal dies with it. herdr is a background server that owns the terminal instead, so the agent keeps going and your layout comes back when you reattach.
This assumes you already run a coding agent CLI. Claude Code, Codex, Cursor, opencode, Grok and about a dozen others are supported.
It assumes Linux or macOS. Windows is in beta.
You need curl and awk. Nothing else.
Versions here are herdr 0.8.0 on Ubuntu.
The documented install is a pipe straight into a shell:
curl -fsSL https://herdr.dev/install.sh | sh
Do not do that. Download it and read it first.
curl -fsSL https://herdr.dev/install.sh -o herdr-install.sh
less herdr-install.sh
sh herdr-install.sh
This one holds up, which is worth saying because most do not.
sudo. It installs to ~/.local/bin.~/.local/bin is missing from
PATH and leaves you to fix it.Confirm it landed with command herdr --version
If that says command not found, add the directory to your path:
export PATH="$HOME/.local/bin:$PATH"
Run herdr status
client:
version: 0.8.0
channel: stable
protocol: 19
server:
status: not running
socket: /home/you/.config/herdr/herdr.sock
Client and server are separate. The server is the part that keeps your agents alive.
Everything runs over that UNIX socket. There is no network port.
Run herdr and you get the session, attached.
Detach with Ctrl+B then Q. The prefix is tmux style.
Reattach by running herdr again.
To run the server without attaching, use command herdr server
Stop it with command herdr server stop
Named sessions work too:
herdr --session work
herdr session list
herdr session attach work
Get this wrong and you build the wrong layout. I did.
A session is a whole separate server. Its own directory, its own socket, its own
process. Check with command herdr session list
name status directory socket
default running ~/.config/herdr ~/.config/herdr/herdr.sock
work running ~/.config/herdr/sessions/work ~/.config/herdr/sessions/work/herdr.sock
They share nothing. A workspace in one is invisible from the other. A crash in one cannot touch the other.
A workspace lives inside a session. One session can hold as many as you want, and you see them all in a single attach.
So pick by what you want:
This is the thing that decides the question above, and I learned it the hard way.
Print the defaults with command herdr --default-config and look at what exists:
workspace_picker = "prefix+w"
next_workspace = ""
previous_workspace = ""
switch_workspace = ""
Every one of those is a workspace. There is no session equivalent.
So sessions are not something you move between while you work. You attach to one, and to reach another you detach and attach again with a different flag.
I built six named sessions for six projects, attached, and saw an empty screen. All six were running
fine. A bare herdr lands on the default session, and that one was empty.
The rule: use sessions only for isolation you never need to cross. Anything you want to move between during a working day has to be workspaces, because that is the only axis with a keybinding on it.
Six workspaces in one session gives you a bare herdr that shows all six, and
prefix+w to pick between them.
Target a named session by putting the flag before the subcommand:
herdr --session work workspace list
herdr --session work agent list
HERDR_SESSION=work works too.
The default session takes no flag at all. It is the unnamed one.
w1:p1 exists in every session you create and means something different in each.
Copy a pane id out of one session's output and run it against another and you will hit the wrong pane, or get an error if you are lucky.
Always pair a pane id with the session it came from.
Install the integration for the agent you use:
herdr integration install claude
Swap claude for codex, cursor, opencode,
grok, copilot, droid, kimi and the rest. List them
with command herdr integration list
This is what makes herdr show whether a pane is working, blocked or idle instead of just alive.
It does two things, and it tells you:
installed claude integration hook to ~/.claude/hooks/herdr-agent-state.sh
ensured claude settings at ~/.claude/settings.json
It adds a hooks.SessionStart entry to ~/.claude/settings.json.
Back that file up first if you already have hooks in it. Mine had none, so it only added a key and left permissions and the rest untouched. I would not assume that for a file that already has a hooks block.
The hook file also carries a warning in its own header. It is managed by herdr, and reinstalling or updating overwrites it. Put custom hooks in a separate file beside it, never inside it.
Read it before you trust it. It is short.
The first thing it does is give up unless it is inside a herdr pane:
[ "${HERDR_ENV:-}" = "1" ] || exit 0
[ -n "${HERDR_SOCKET_PATH:-}" ] || exit 0
[ -n "${HERDR_PANE_ID:-}" ] || exit 0
So it is a no-op in every normal Claude session. That is the property that matters. Installing the integration does not change how Claude behaves outside herdr.
Inside a pane it sends your session_id and the path to your transcript to the local socket.
That is how the status indicator knows what the agent is doing.
Worth knowing if you care where your transcript path goes. It is a local UNIX socket and it never leaves the machine, but it is your transcript path.
The whole thing is wrapped so a failure cannot take your session down with it.
This is the part that makes herdr more than a nicer tmux.
Every pane is scriptable over the socket, and the same API the TUI uses is exposed as commands.
herdr workspace create --cwd ~/project --label build
herdr pane list
herdr pane split w1:p1 --direction right --cwd ~/project
herdr pane read w1:p1 --source visible --lines 40
herdr pane close w1:p2
Panes are addressed as workspace:pane, so w1:p1 is the first pane of the first
workspace. Tabs are w1:t1.
Most commands return JSON. Pipe them through python3 -m json.tool when you are reading them
by eye.
Once a pane hosts a detected agent you get a second set of commands:
herdr agent list
herdr agent prompt build "run the test suite" --wait --until idle
herdr agent read build --lines 60
herdr agent send-keys build Escape
herdr agent wait build --until idle --timeout 600000
herdr agent prompt --wait --until idle is the useful one. It sends a prompt and blocks until
the agent goes idle, which makes an agent scriptable from a plain shell script or a cron job.
Make a workspace with the repo as its working directory:
herdr workspace create --cwd ~/your-repo --label your-repo
That returns a root pane id, normally w1:p1.
Start the agent in it:
herdr agent start myrepo --kind claude --pane w1:p1
myrepo is the name you will use to talk to it. Confirm with command
herdr agent list
name=myrepo kind=claude status=idle pane=w1:p1 cwd=/home/you/your-repo
Now drive it by name:
herdr agent prompt myrepo "summarise the last five commits" --wait --until idle
herdr agent read myrepo --source recent --lines 40
Pass flags to the agent itself after a double dash:
herdr agent start myrepo --kind claude --pane w1:p1 -- --model opus
Start an agent from inside another agent's shell and it picks up that shell's variables.
I did exactly this and the new pane came up with a banner reading
Transcript saving is off, inherited CLAUDE_CODE...
Nothing was broken. It just was not the config I thought I was starting.
Start your agents from a clean shell, or pass what you want explicitly with
--env KEY=VALUE on workspace create or pane split.
Point an agent command at a plain shell pane and you get this:
{"error":{"code":"agent_not_found","message":"agent target w1:p1 not found"}}
That is correct behaviour, not a bug. herdr agent only targets panes where an agent was
detected.
herdr pane read works on any pane, agent or not. Use that when you just want to see what a
shell printed.
This is the setup worth building. Pick one box that is always on and run the server there. Every other machine is a viewer.
herdr --remote user@alwayson-box
herdr --remote user@alwayson-box --session work
Your agents run on that box. Close the laptop, walk to the desktop, attach from there, and the work is where you left it.
Keybindings default to local. Use --remote-keybindings server if you would rather the
remote config win.
--remote is a client command. You type herdr on the machine you are sitting at,
so yes, that one needs the binary installed locally.
You can skip that entirely:
ssh -t alwayson-box herdr
That runs the client on the server box. Your laptop needs nothing but an SSH client.
Pick on this:
--remote. Better native terminal behaviour and your
own keybindings, at the cost of keeping versions in step on every machine.That last cost is real. herdr status reports a protocol number and a compatibility line:
client:
protocol: 19
server:
protocol: 19
compatible: yes
Two machines on different herdr versions can land on different protocol numbers. Start with plain SSH. Install locally when you want the nicer client, and then update every machine together.
The installer puts herdr in ~/.local/bin, which your interactive shell adds to
PATH and a non-interactive SSH command does not.
So this fails even though it works fine when you are logged in:
$ ssh -t box herdr
bash: line 1: herdr: command not found
Check what you actually get with command ssh box 'echo $PATH'
Fix it once by linking the binary somewhere already on that path:
sudo ln -sfn ~/.local/bin/herdr /usr/local/bin/herdr
Link it, do not copy it. herdr update replaces the file in ~/.local/bin, and a
link follows the update while a copy silently goes stale.
Run it as a systemd user service on the always-on box.
Create ~/.config/systemd/user/herdr.service:
[Unit]
Description=herdr server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=%h/.local/bin/herdr server
ExecStop=%h/.local/bin/herdr server stop
Restart=always
RestartSec=3
Environment=PATH=%h/.local/bin:/usr/local/bin:/usr/bin:/bin
[Install]
WantedBy=default.target
Enable it with command systemctl --user enable --now herdr.service
That unit covers the default session only. For named sessions, use a template unit so each one is its own
managed service. Save this as ~/.config/systemd/user/[email protected]:
[Unit]
Description=herdr server (session %i)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=%h/.local/bin/herdr server --session %i
ExecStop=%h/.local/bin/herdr --session %i server stop
Restart=always
RestartSec=3
Environment=PATH=%h/.local/bin:/usr/local/bin:/usr/bin:/bin
[Install]
WantedBy=default.target
Then start as many as you want, each one persistent on its own:
systemctl --user enable --now [email protected]
systemctl --user enable --now [email protected]
Note herdr server takes --session even though
herdr server --help does not list it.
A user service stops with your last session unless you turn lingering on.
Enable it with command sudo loginctl enable-linger $USER
Check it with command loginctl show-user $USER | grep Linger
Miss this and everything above looks correct and still dies the moment you disconnect. That is the whole point of the exercise, so check it rather than assume.
Confirm the service really self heals. Kill it and watch it come back:
systemctl --user show herdr.service -p MainPID --value
kill -9 <that pid>
systemctl --user show herdr.service -p MainPID --value
A different pid and an active service means it works.
This is the thing that surprised me most, and it matters if you plan to leave work running.
Restart the server and your workspaces and panes come back. The agent processes do not. They are children of the server. You reattach to a familiar layout full of bare shells.
Worse, herdr tries to be clever about it. On restart it relaunches the agent with a resume flag pointed at the old session id:
claude --resume b48be71c-2128-414d-8dd8-adf1d66d0ac6
No conversation found with session ID: b48be71c-2128-414d-8dd8-adf1d66d0ac6
When that transcript does not exist, the resume fails and you are dropped to a shell.
Which is exactly what happens if transcript saving was off in that pane. The environment inheritance trap above and this one are the same bug wearing two hats.
Write a small script that starts the standing set, and make it idempotent so it leaves running agents alone. The important line is the check:
if herdr agent list | grep -q "\"name\":\"$name\""; then
echo "ok $name already running"
continue
fi
Never restart a live agent. That throws away the context it is holding, which is the whole reason you are running herdr.
Then run it once at boot, after the server:
[Unit]
Description=Restore standing herdr agents
After=herdr.service
Requires=herdr.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=%h/.local/bin/herdr-restore-agents.sh
[Install]
WantedBy=default.target
Have it wait for the server before it does anything. On boot the script will win the race otherwise:
for _ in $(seq 1 30); do
herdr status 2>/dev/null | grep -q 'status: running' && break
sleep 2
done
Do not put this on a timer. A timer resurrects an agent you closed on purpose. Run it at boot, and by hand if the server ever crash restarts on you.
A small thing that cost me a duplicate workspace.
My restore script looked up a workspace by label and started an agent in it. I had labelled the workspace
wiki-repo and named the agent wiki, so the lookup missed and it built a second
workspace next to the first.
Pick one name per project and use it for both.
Rename an existing one with command herdr workspace rename w4 soat
Being straight about it, because a guide that implies more than it checked is worse than a short one.
What I did check:
I have not run a multi-hour job under it, or pulled the network out mid-task, or attached from a second machine over SSH.
So the survives a dropped connection claim is still theirs, not mine. Try that one yourself before you move a real workflow over.
Update with command herdr update
Switch channels with command herdr channel set preview
To remove it, stop the server, delete the binary, and pull the integration back out:
herdr server stop
herdr integration uninstall claude
rm ~/.local/bin/herdr
rm -rf ~/.config/herdr
Check ~/.claude/settings.json afterwards and confirm the hooks entry is gone.
A tool that installs a hook into another tool's config is doing something you should look at, even when it is honest about it.
herdr was honest. It printed both paths it touched, the hook is readable in one screen, and it no-ops outside its own environment.
That took two minutes to confirm and it is the difference between running software and trusting it.