The agent on the homelab box finished eleven minutes ago. Nothing told you, because its terminal lives behind an ssh session in a window you are not looking at. Meanwhile the sidebar in front of you shows four local agents and reports their state perfectly.
This post is for anyone who runs coding agents on more than one machine and
wants a single window that shows all of them. It covers the herdr-mirror
plugin: what each end of the link needs, how to set up the access it depends
on, and how the connection starts. This setup uses a laptop as the local
machine and a homelab server called homelab as the remote.
The plugin makes each remote workspace a real local workspace named
<host>: <name>. Its panes stream the remote terminal live, and its agents
report their true state in the sidebar. Mirroring is one way, but you can type
into any mirror pane to drive the remote session.
What you need on each machine
The requirements are lopsided. Almost everything happens on the local machine, and the remote runs stock herdr and nothing else.
Requirements on the local machine
The local machine carries the plugin, the configuration, and the daemon that holds the connection open.
- herdr, on a build whose command line offers
herdr terminal session. - macOS or Linux on x86_64 or aarch64, because the installer fetches a prebuilt binary.
- The
herdr-mirrorplugin, installed and enabled. - A
hosts.tomlfile listing each remote you want to mirror. - Non-interactive ssh key access outbound to every host in that file.
Check the stream support first, because a version number does not answer this requirement clearly:
herdr terminal --help
The output must list a session subcommand. If it does not, the build is too
old and no amount of configuration helps.
Requirements on the remote machine
The remote needs herdr and a way in. It does not need the plugin, a configuration file, an open port, or any knowledge that mirroring exists.
- herdr installed, on a build that also offers
herdr terminal session. - A herdr server running, which is the requirement people miss.
- Your public key in
~/.ssh/authorized_keys. socatorpython3, but only if the ssh daemon refuses socket forwards.
The last item covers a transport fallback. The plugin reaches the remote
application programming interface (API) socket through an ssh -L forward by
default. Some ssh daemons accept the forward
and then never move a byte, so the plugin falls back to a relay over ssh
exec, which needs socat or python3 on the remote. Almost every system has
one of them.
Keep both ends on the same herdr build. The streaming protocol is the moving part, and a mismatch shows up as a version complaint from the daemon rather than a broken pane.
Setting up key-based access
The plugin runs unattended, so it cannot answer a password prompt. Its access test is exact:
ssh -o BatchMode=yes homelab true
That command must exit zero and print nothing. A dedicated key keeps this access separate from your general ssh identity, which means you can revoke it by deleting one line on the remote.
Generate the key on the local machine:
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_herdr -N "" -C "herdr-mirror"
The empty passphrase is a real trade-off, not an oversight. A daemon that
reconnects on its own cannot prompt you, so the choice is an unencrypted key
or a passphrase held by an agent that must stay loaded. On macOS the agent
route works well with UseKeychain yes and AddKeysToAgent yes. The
unencrypted key is simpler and survives agent problems, at the cost that
anyone who reads the file gains shell access to the remote.
Copy the public half to the remote. This step prompts for the remote account password, so run it yourself in a terminal:
ssh-copy-id -i ~/.ssh/id_ed25519_herdr.pub user@homelab
Then name the target in ~/.ssh/config so the dedicated key is always the one
offered:
Host herdr-homelab homelab
HostName homelab.lan
User user
IdentityFile ~/.ssh/id_ed25519_herdr
IdentitiesOnly yes
IdentitiesOnly yes prevents a confusing authentication failure. Without it,
ssh offers every identity the agent holds, and a server with a low
MaxAuthTries can reject you before it reaches the right key.
Now run the access test again. Nothing later works until it passes silently.
Keeping the remote server alive
A herdr server on the remote is a requirement, not a detail, and a server started by hand disappears at the next reboot. A systemd user unit fixes both problems.
Write ~/.config/systemd/user/herdr.service on the remote:
[Unit]
Description=herdr server
After=network.target
[Service]
Type=simple
ExecStart=/home/user/.local/bin/herdr server
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
Use an absolute path in ExecStart. Systemd does not read your shell profile,
so a bare herdr fails when the binary lives in ~/.local/bin. Find the real
path with command -v herdr.
Enable and start it:
systemctl --user daemon-reload
systemctl --user enable --now herdr.service
herdr status server
The status output should report status: running along with a protocol
number and a socket path.
One step remains, and it needs root:
sudo loginctl enable-linger user
Lingering keeps the systemd user manager running when you have no login session on the machine. Without it, the user manager exits after your last session closes and takes the herdr server with it. The setup appears to work for a while, because the mirror daemon keeps connecting over ssh and creating sessions, then fails after a reboot. Enable lingering and the problem never appears.
Installing and configuring the plugin
Everything in this section happens on the local machine.
herdr plugin install nikok6/herdr-mirror
herdr server reload-config
The install prints a preview of the actions and event hooks the plugin
registers, then builds the binary. It also links a command
at ~/.local/bin/herdr-mirror, which gives shell use and key bindings one
stable path that survives updates.
Next, list your hosts:
[hosts.homelab]
target = "herdr-homelab"
The key on the left, homelab, becomes the sidebar prefix, so mirrored
workspaces appear as homelab: <name>. The target value accepts anything
ssh accepts, which includes an alias from ~/.ssh/config. Pointing it at the
alias rather than at user@host guarantees the dedicated key applies.
The file belongs at ~/.config/herdr-mirror/hosts.toml, and that path is
literal. The plugin does not honour XDG_CONFIG_HOME, so a configuration
directory relocated by that variable is not searched. This matters if you keep
your configuration in a repository and point XDG_CONFIG_HOME at it, which is
a common dotfiles pattern. Run herdr-mirror status to see the exact paths it
searches and the file it loaded.
Starting the connection
There is no connect command, which surprises most people. Three layers stack up, and each one only needs the layer under it:
- The remote server: a running herdr server publishes an API socket. The plugin reads workspace and pane state from it.
- The transport: the local daemon opens an ssh master connection, then
forwards that API socket over it. The
api_transportsetting selects the method, and its default ofautotries the socket forward first and falls back to the exec relay. - The daemon: one local process reconciles remote workspaces into local mirrors and pushes agent status into the sidebar.
Only the third layer needs starting, and it starts itself. The default
autostart = true means that focusing any workspace starts the daemon. To
start or resume it by hand:
herdr-mirror start
Reconnection is automatic. The daemon retries on a ladder rather than giving
up, so a remote that reboots comes back on its own. A manual herdr-mirror
pause is sticky until you run start again, while a crash recovers at the
next workspace focus.
Verifying the setup
Work through these checks in order. Each one isolates a different layer, so the first failure tells you where the problem lives.
- Confirm silent key access with
ssh -o BatchMode=yes homelab true. - Confirm the remote server with
ssh homelab 'herdr status server'. - Confirm the local server with
herdr status server. - Confirm the plugin loaded with
herdr-mirror status. - Create a workspace on the remote and watch it appear locally.
The fourth check is the informative one. A healthy daemon reports the host as connected and prints the recent log:
daemon: running (pid 45137)
config: /Users/user/.config/herdr-mirror/hosts.toml
host homelab (herdr-homelab): 0 mirror workspaces, 0 mirror panes
recent log:
2026-08-14T11:35:25.997Z [homelab] connected and synced
Zero mirror workspaces is correct on a fresh remote server, not a fault. The plugin mirrors the workspaces that exist, and a new server holds none. Create something for it to mirror:
ssh homelab 'herdr workspace create --label mirror-test --no-focus'
Within a few seconds the daemon log records the result, and homelab:
mirror-test appears in the local sidebar:
2026-08-14T11:38:41.106Z creating mirror workspace homelab: mirror-test
host homelab (herdr-homelab): 1 mirror workspaces, 1 mirror panes
Reading the daemon log when it fails
The daemon log names the layer that failed, which makes it faster than guessing. Three messages cover most first-time setups.
ssh master to user@host failed: Permission denied (publickey,password)
tells you the transport layer cannot authenticate. The key is missing from the
remote, or ssh offers the wrong identity. Reproduce it outside the plugin with
ssh -o BatchMode=yes homelab true, which fails the same way and reports more
detail with -v.
remote herdr server is not running tells you the transport works and the
remote server does not. Authentication has already succeeded at this point, so
this message is progress. Check the systemd unit with
systemctl --user status herdr.service on the remote.
connected and synced with zero mirrors tells you everything works and
the remote has no workspaces yet. Create one and watch the count change.
A renamed host key needs a daemon restart, because the running daemon holds
the loaded configuration. Run herdr-mirror pause followed by
herdr-mirror start, then check that the log lines carry the new prefix.
After the first connection
Nothing on the local machine needs starting a second time. The daemon owns
reconnection, so a remote that reboots returns on its own with the same
workspace and pane counts. Reach for herdr-mirror start only when
herdr-mirror status reports the daemon stopped or paused.
Working inside a mirror has its own rules, and they are the subject of the second part: Working inside a herdr mirror.
