← Back to Wiki
Terminal / AI Agents

herdr: Keep Your Coding Agents Running After You Close the Laptop

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.

Share on X

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.

Read the install script before you run it

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.

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"

Check the server before you do anything else

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.

Start it

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

Sessions and workspaces are not the same thing

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:

BE WARNED: there is no keybinding to switch sessions

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.

BE WARNED: pane ids are only unique inside a session

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.

Wire up your agent

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.

BE WARNED: the Claude integration writes to your settings.json

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.

What that hook actually does

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.

Driving panes from the CLI

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.

Driving the agent itself

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.

Starting an agent in a specific repo

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

BE WARNED: the agent inherits the environment you start it from

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.

agent commands need an agent, pane commands do not

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.

One server, many computers

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.

Do you need herdr on every machine? Two answers

--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:

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.

BE WARNED: your PATH is different over SSH

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.

Make the server survive a reboot

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.

BE WARNED: without lingering this dies when you log out

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.

BE WARNED: the layout survives a restart, your agents do not

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.

Put your agents back automatically

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.

Name the workspace the same as the agent

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

What I did not test

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.

Updating and getting rid of it

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.

The general lesson

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.