Agentic and local, anywhere, at all times

20 minutes read •

The last piece is the agent. The Any-Port Dev Tunnel carries the ports; this carries the thing you actually direct: herdr keeps a headless fleet of coding agents alive on the box, --remote re-attaches to it as a thin client from anywhere, a standing roster of eight seats gives the fleet shape, and a relayed SSH transport with post-quantum key exchange makes “anywhere” a plane, a hotel and a café, not a LAN.

Read it first

The prompt walks an assistant through the whole setup: the headless herdr fleet under a restart-on-boot unit, the eight seat files, and a relayed transport wired with PQ key exchange so the box is reachable by name from any network. It assumes the box from The Any-Port Dev Tunnel, a Linux box you can SSH into, or any other.

The other handoff

Part one moved the heavy environment onto the box (the servers, the builds, the ports) and carried the browser back to you. That left the one thing you actually direct still glued to your terminal: the agent.

As it stands, you SSH in, drop claude-code or codex into a pane, and you are the process manager. Walk away and the terminal is a corpse; the agent is mid-task and the box has no idea you were ever there. Reconnect and you rebuild the scene by hand: which pane was which, what it was holding, where the handoff point was. The agent is the least local thing in your setup, and it is the one you want everywhere.

The fix is to make the agent session the thing that is always on, and make yourself the thin client. Run herdr as a headless server on the box so the fleet (workspaces, tabs, panes, the agents inside them) survives you. Re-attach with herdr --remote as a pure view: close the laptop, the fleet keeps running; open it again, you are where you were. Give the fleet a standing roster of seats, named roles with an ownership boundary, so “agentic” has a shape and a repeatable crew instead of a pile of anonymous terminals. Then put the box behind a relayed SSH transport with the key exchange pinned to post-quantum, so “anywhere” stops meaning “on my network.”

The Any-Port Dev Tunnel is where the box, the SSH entry and the ports came from. This is the half that keeps your agents alive there.

How it works

Three layers, and only one of them is your laptop:

Laptopthin clientherdr --remoteRelayed pipeclassical cryptono inbound port neededone key per consumersshPQ key exchangeDev boxheadless herdr server8 standing seatsloopbackLocal modelllama-server, optional

1 · The fleet. herdr runs as a headless server on the box. It owns the workspaces, tabs and panes, and it watches each pane: it knows whether the agent inside is idle, working, blocked or done, and it sets HERDR_ENV=1 in managed panes so an agent can tell it is under herdr. Under a restart-on-boot unit, the fleet is alive whether or not you have logged in.

2 · The thin client. On any machine, herdr --remote devbox --session main opens an SSH connection to the box and renders its fleet into your terminal. You are a view, not the owner: detach here and it is waiting where you attach next. No flag moves that attach between devices: the server holds the state, so showing up somewhere else is just running the same command from there.

3 · The seats. A directory of agent files (one per seat) defines the crew: the role, what it owns, the facts it never re-derivates, what it never does, and what good output looks like. The fleet fills the seats; you stop spawning anonymous agents and start staffing a team.

4 · The network. A relayed transport puts the box behind an address that resolves from any network, with no inbound port to open and no local interface to protect. Classical crypto only, since that layer moves bytes, not secrets. The actual SSH session riding through it is where the security lives, and its key exchange is pinned server-side to post-quantum algorithms. The devbox SSH entry points at that address, so herdr --remote works from anywhere the relay can reach, which is everywhere you are.

Before you start

  • A box that stays on. The same Linux box from part one, or any other. It becomes a server, not a laptop. It does not need to be switched on when you are out.
  • herdr installed on the box, and (optionally, for a nicer local attach) on your laptop. Everything here is verified against herdr --help for v0.8.2; run herdr --help on yours if a flag has moved.
  • A relay client (this article uses tailcat, a userspace tool built on the same WireGuard/relay data plane Tailscale uses, without its control plane) on the box and on your laptop. No firewall change needed: it dials out, it does not open an inbound port.
  • An OpenSSH client and server new enough for post-quantum key exchange (≥9.0 has sntrup761x25519-sha512; ≥9.9 defaults to mlkem768x25519-sha256 too); check with ssh -V on both ends.
  • SSH key auth already working between your machine and the box; part one's devbox entry is the starting point.
  • A GPU, if you want part five. Everything through the relay works fine talking only to cloud agents; the local model is optional, and its own step names what card size it assumes.
PlaceholderMeaningExample
devboxthe ~/.ssh/config Host entry for the boxdevbox
<relay-address>the box's resolved relay address, pinned to a fixed region so it doesn't rotatetXXXXXXXXXXXXXXXX
<client-pubkey>your laptop's relay public key, allow-listed on the boxnodekey:XXXXXXXXXXXXXXXX
$USERthe unix user the fleet runs asdev

01 Stand up the persistent fleet

On the box, run herdr as a headless server. That is the whole trick: the thing you attach to keeps running when you are gone.

herdr, headless
herdr server

herdr server owns the workspace tree. Every pane it manages gets HERDR_ENV=1 in its environment, and herdr reads each pane to classify its agent (idle, working, blocked or done), so when you attach you see live state, not a blank terminal. Run it under a systemd user unit so it comes up at boot and restarts if it dies, and linger so it survives your last logout:

~/.config/systemd/user/herdr-fleet.service
[Unit]
Description=Herdr agent fleet (headless)
After=network-online.target
Wants=network-online.target

[Service]
Environment=HERDR_SESSION=main
ExecStart=%h/.local/bin/herdr server
Restart=always
RestartSec=10

[Install]
WantedBy=default.target
start it and make it stick
systemctl --user daemon-reload
systemctl --user enable --now herdr-fleet.service
sudo loginctl enable-linger "$USER"
systemctl --user status herdr-fleet.service --no-pager

The HERDR_SESSION name is how a named fleet is addressed; herdr --session main on the client side attaches to it by that name. enable-linger is the same move as part one: the box keeps the fleet warm after you log out, instead of tearing it down. Check status shows active (running) and that the unit survived a logout.

What “persistent” means now

The agents are no longer children of your SSH session. Kill the terminal, close the laptop, reboot the box: the workspace tree, the panes and the agents' on-disk state are owned by the herdr-fleet unit, not by your login. That is the difference between a tool you run and a place you work.

02 Attach as a thin client

From your laptop, herdr is just a viewer for the box's fleet.

attach over SSH
herdr --remote devbox --session main

--remote devbox opens an SSH session to the box (using the part-one devbox entry) and --session main names the fleet you set up in part one; leave it off and you attach to (or silently create) an unrelated default session, not the one the unit is running. Once attached you are a pure view: type, and the keystrokes reach the agent on the box; the agent's output streams back. Close the laptop and nothing on the box changes: the fleet is still there, still working or blocked or done, exactly as you left it.

Moving to a second device needs nothing special: run the same command from wherever you are. The persistent server is the whole trick: there is no attach to hand off, because nothing about the fleet lived on the device you just walked away from.

A remote attach can start a server, not just find one

herdr --remote does not require an existing headless server the way it looks like it should: if herdr-fleet is not running, a remote attach can spin one up on the spot (installing the binary interactively if it's missing). That is convenient until it isn't: a dead systemd unit gets masked by a fresh, unmanaged, non-persistent session that looks fine until you close the laptop and it dies with you. If an attach ever feels emptier than it should, check the unit's status on the box before trusting what's in front of you.

03 Lay down the seats

A fleet with no crew is a screen of terminals. Give it seats: one file per role, living on the box, each with the same five-part shape.

the seat skeleton
You are the <Seat> for <product>. Your north star is <one sentence>.

You own:
- <the surface you are accountable for>
- <the decisions you make without asking>
- <the recurring cadence you run>

Facts you never re-derive (read the file, don't guess):
- <the seams, where this seat meets the others>

You never:
- <the hard boundaries; what is out of scope>

Good output here looks like:
- <the concrete shape of a finished piece of work>

The crew in a working roster is eight seats. Generalized to any product, they are:

SeatOwnsNever
Chief of Staffthe owner's left hand: briefings, sequencing, unblocking, the meeting recorddoesn't build the product or spend the money; routes execution, doesn't do it
Research Leadevidence: the sourced answer to any “do we know” questiondoesn't contact anyone or act on a finding; only surfaces sourced evidence
Content Leadthe words: docs, changelog, comms, the marketing surfacedoesn't publish anything; every draft waits for the owner
Engineering Leadscopes and reviews the work: briefs an implementation, checks what comes back against the standardsdoesn't implement or deploy itself; briefs and reviews, execution is routed and gated
Design Leadthe look and the interaction: components, copy-in-context, the system's integritydoesn't write product code or push a change; specs it, never ships it
Platform Opsthe judgment queues: what needs a human call (applications, reviews, consequences for a broken commitment)doesn't decide any of it; prepares every recommendation, executes none
Financemoney in and out: pricing, unit economics, the ledger, compliancedoesn't move a cent or touch a credential; prepares the numbers, the owner clicks
Domain seatthe relationship: your customers, partners, community; target lists, outreach draftsdoesn't send anything; every outreach stays a draft until it's approved

The domain seat is the only one you rename per product. The other seven, Chief of Staff through Finance, are stable. Notice the pattern in the Never column: every seat prepares, none of them execute. Engineering Lead briefs and reviews, it doesn't push. Design Lead specs, it doesn't ship. Platform Ops recommends, it doesn't decide. That's not caution for its own sake. It's a gate, and it's what makes an eight-agent fleet safe to leave running unattended: nothing leaves the seat as a finished action, everything leaves it as a draft the owner still has to move. A worked example, generalized:

docs/agents/chief-of-staff.md
You are the Chief of Staff for <product>. Your north star is that the
owner never has to hold more than one decision at a time.

You own:
- the daily/weekly briefing: what moved, what's blocked, what needs a call
- sequencing: which seat works next, and why
- the meeting record and the follow-up list, closed to zero

Facts you never re-derive (read the file, don't guess):
- the seat roster and each seat's scope (docs/agents/)
- the open follow-ups and their owners (docs/ops/)

You never:
- build product, write copy for publication, or move money
- make a seat's call for it; you surface the decision, the seat owns it

Good output here looks like:
- a briefing in three sections (moved / blocked / needs-a-call), each item
  one line, each blocker naming the seat that can clear it

Put the eight files in docs/agents/<seat>.md on the box. When you spawn an agent in a herdr pane, you point it at its seat file: the file is the job description. The fleet stops being a pile of terminals and becomes a team that already knows its roles.

Eight standing seats fit a small crew doing bounded, reviewable work. For work that's actually parallel — a hundred independent items getting the same treatment — the alternative isn't more seats, it's a fan-out: one coordinator decomposes the task, spawns a swarm of disposable sub-agents (one per item, no persistent role, no seat file), and a validator checks the merge before it's called done. @eng_khairallah1 put the shift well: “one instruction in, a hundred workers spawned, one assembled deliverable out.” Different shape for a different job: seats for judgment that has to stay reviewable, a swarm for volume that's embarrassingly parallel.

04 A relayed pipe with a post-quantum lock on the door

Right now devbox points at a LAN IP, so the fleet is reachable from home and nowhere else. This part has two layers, and they solve different problems: a relay gets a byte stream to the box from anywhere with no inbound port to open; the actual SSH session inside that stream is where the real security decision gets made, and it's made server-side, not client-side.

Tailscale did this part fine for a long time, and if you want its account and control plane along with the plumbing, it still will. This article has moved to tailcat instead, now that it's out: a small userspace tool that rides Tailscale's own WireGuard-and-relay data plane without needing Tailscale's account or control plane. It moves bytes with ordinary classical crypto. That's fine, because it isn't the layer carrying your session's actual security. Any relay that gets a byte stream from your laptop to the box works the same way.

keys, once each
# on the box: a server key, region pinned so the address never rotates
tailcat genkey --key=default --fixed-region
# -> prints the address blob; keep it, you'll resolve it below

# on your laptop: a client key
tailcat genkey --client --key=client-default
# -> prints your client's public key; hand it to the box's owner
Pin the region before you publish the address

Without --fixed-region, the server key re-picks its relay region by latency on every restart, and the address blob it prints changes with it. Fine for a one-off test; useless for anything you're about to put in an SSH config or a DNS record. Pin it first, then treat the printed address as permanent.

On the box, run the relay as a systemd user unit, locked to that one client key from the start:

~/.config/systemd/user/tailcat-ssh.service
[Unit]
Description=tailcat relay for SSH access
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=%h/.local/bin/tailcat serve --key=default --allow=nodekey:<client-pubkey> 22
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target
start it and resolve a durable address
systemctl --user daemon-reload
systemctl --user enable --now tailcat-ssh.service
tailcat resolve <address-blob>   # a longer, self-contained token; no DERP-map fetch needed to connect

That relay forwards raw bytes to port 22 on the box; it never sees or terminates SSH. So the real key exchange happens exactly the way it would over a LAN, between the real client and the real sshd, and that's where you pin post-quantum:

on the box (needs root)
echo 'KexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512' \
  | sudo tee /etc/ssh/sshd_config.d/99-pq-kex.conf
sudo sshd -t                        # validate before reloading
sudo systemctl reload sshd          # SIGHUP: re-reads config, keeps active sessions

This is server-side enforcement: it applies to every client that connects, tailcat or not, regardless of what that client asks for. mlkem768x25519-sha256 (ML-KEM768+X25519, an IETF draft, OpenSSH's default since 9.9) and sntrup761x25519-sha512 (Streamlined NTRU Prime+X25519, OpenSSH-native since 9.0) are both hybrids: classical X25519 stays in the mix, so a break of the post-quantum half alone doesn't break the exchange. An old client without either algorithm doesn't get downgraded quietly: it fails outright, with a mismatch it reports plainly. The usual objection to a hybrid handshake (a few extra kilobytes and a round trip, real money on a slow satellite link) doesn't apply to this hop: the negotiation is between tailcat's local proxy and sshd, both talking over loopback, not across the relay itself. The relay just hands over raw bytes; the handshake it's carrying never leaves the box.

Then repoint the devbox entry in ~/.ssh/config to dial through the relay. tailcat in client mode is a raw byte pipe given an address and a port, exactly what an OpenSSH ProxyCommand wants, so the SOCKS tunnel from part one stays, now flowing over that pipe. Swap only HostName for ProxyCommand; leave every other line, including ServerAliveInterval, ServerAliveCountMax and ExitOnForwardFailure if part one had you add them, exactly as it was:

~/.ssh/config
Host devbox
    ProxyCommand tailcat <resolved-address-blob> 22
    User dev
    DynamicForward 1080
    ServerAliveInterval 30
    ServerAliveCountMax 3
    ExitOnForwardFailure yes
The old tunnel doesn't reconnect on its own

If part one's persistent SOCKS service (autossh under devbox-socks.service, or its launchd/Task equivalent) is already running, it holds an SSH connection made under the old HostName line and won't pick up the new ProxyCommand until it reconnects. Restart that service now, then rerun part one's own curl --socks5-hostname check before trusting the browser side again.

One relay key, one consumer

The client key above authenticates one peer. The moment a second persistent connection shows up — a second forwarded port, a background job, anything that stays open rather than dialing in and hanging up — and reuses that same key, the two race for one WireGuard endpoint slot every time either one has to re-handshake. There is no error for this. The loser just goes quiet: a socket that took the connection and then stopped carrying bytes, which looks exactly like a flaky relay and isn't. Mint one client key per consumer that's going to stay up, not one key shared by everything:

one key per persistent consumer, not one key for all of them
# the box's owner mints one per consumer that's going to stay open,
# plus one more for everything ad-hoc: interactive sessions, one-off commands
tailcat genkey --client --key=client-tunnel      # e.g. the SOCKS service from part one
tailcat genkey --client --key=client-inference   # e.g. a second, unrelated forwarded port
tailcat genkey --client --key=client-default     # ad-hoc only -- never a persistent service

# server side: allow every one of them, not just the first
tailcat serve --key=default \
  --allow=nodekey:<tunnel-pubkey>,nodekey:<inference-pubkey>,nodekey:<default-pubkey> 22

Give each consumer its own Host alias in ~/.ssh/config, each with its own ProxyCommand pointed at its own key, and never let a persistent service and an ad-hoc session share one. If you ever edit one later and forget which is which, the two will keep starving each other in a way that looks exactly like a network problem and isn't one.

A tunnel can be up and not there

ServerAliveInterval and ServerAliveCountMax catch a connection that has actually died. They don't catch one that took the forward, kept the TCP socket open, and then just stopped carrying bytes — which a long-lived hop over a hostile network will eventually do, key collision or not. Restart=on-failure and launchd's KeepAlive don't catch it either, because as far as either supervisor can see, nothing exited. What catches it is a health check that looks past the process into whatever the tunnel is actually supposed to be carrying, and kills the SSH process itself once that check goes quiet:

self-healing wrapper, point the unit or plist at this instead of at ssh directly
#!/usr/bin/env bash
HOST=devbox              # your ProxyCommand host, pinned to one key (see above)
LOCAL_PORT=8080           # whatever this tunnel forwards
HEALTH_URL="http://127.0.0.1:${LOCAL_PORT}/health"   # anything the far end actually answers

start_ssh() {
    ssh -N -o ExitOnForwardFailure=yes \
        -o ServerAliveInterval=10 -o ServerAliveCountMax=3 \
        -L "${LOCAL_PORT}:127.0.0.1:${LOCAL_PORT}" "$HOST" &
    SSH_PID=$!
}

start_ssh
fails=0
while true; do
    sleep 10
    if ! kill -0 "$SSH_PID" 2>/dev/null; then start_ssh; fails=0; continue; fi
    if curl -s -o /dev/null --max-time 8 "$HEALTH_URL"; then
        fails=0
    else
        fails=$((fails + 1))
        [ "$fails" -ge 3 ] && { kill -9 "$SSH_PID"; start_ssh; fails=0; }
    fi
done

Three missed checks, about thirty seconds, and it kills and restarts the connection on its own rather than waiting for you to notice a pane that stopped answering. Nothing here needs the process to actually exit first — that was always the gap a plain supervisor couldn't see.

The self-healing wrapper above assumes it already knows which host to dial. If part one's tunnel picks between a LAN route and this relay on its own — home network available, dial it directly; anywhere else, go through the relay — the thing making that call needs the same discipline as the health check: a real probe against the endpoint, not an inference from this machine's own state.

A subnet match isn't a location check

The obvious shortcut is to read this machine's own interface address and compare its prefix against the LAN's: if it starts with the router's usual 192.168.1., assume you're home and dial the LAN IP directly. It works right up until a network you're actually visiting hands out the same default prefix, which plenty of routers do out of the box. The prefix matches, the picker dials the home LAN IP, that IP isn't reachable from wherever you actually are, and the connect just hangs until it times out — found on an airport network, when the tunnel's port had silently never come up and an unrelated command was the first thing to notice. Nothing crashed; nothing logged an error more specific than a stalled connection. The fix is to stop asking the laptop what it thinks its network is and just ask the LAN endpoint whether it's there, with a timeout short enough that a wrong guess costs milliseconds instead of a hung connection:

pick a route by probing it, not by matching a subnet
pick() {
    # Bounded-timeout probe against the real endpoint. Reading this
    # machine's own interface prefix and comparing it to the LAN's is
    # not a location check: 192.168.1.0/24 is also the out-of-the-box
    # default on plenty of routers that are not this one, so the
    # prefix can match on a network you're only visiting -- and then
    # the picker dials a LAN IP that isn't there and just hangs.
    if nc -z -G 2 "$LAN_IP" 22 2>/dev/null; then
        echo devbox-lan
    else
        echo devbox-relay
    fi
}

while true; do
    host=$(pick)
    ssh -N -D 1080 -o BatchMode=yes -o ExitOnForwardFailure=yes "$host"
    sleep 10
done

nc -z -G 2 bounds the probe itself to two seconds, regardless of the OS's own default connect timeout (-G is macOS's per-connection timeout flag; Linux's nc takes a plain -w instead). Measured against a real LAN endpoint that's actually there, the probe costs under 50 ms — cheaper than the SSH handshake it's deciding between, so there's no real case left for the shortcut once a direct probe is safe to make. The one case where the interface-prefix shortcut used to earn its keep: some relay clients leave a stale route to the LAN IP behind even after being stopped, which makes a direct probe hang for the better part of a minute instead of failing fast. Check whether that's still true for yours before assuming it — a client that's been fully removed doesn't leave the route behind either.

If you'd rather glance than grep a log: tailcat's menu-bar companion, TailcatMenu, shows whether a client identity's relay connection is actually up — useful for ruling out “the relay itself is down” before you go digging into which route a picker like the one above chose.

Now the loop is closed

herdr --remote devbox --session main dials the relay, which pipes straight through to the box's real sshd, which insists on a post-quantum key exchange before it says another word: from an airport, a hotel, a tethered phone. No firewall rule to write, either: the relay dials out to reach the box, it doesn't open an inbound port, so there's no local interface to protect. That is “agentic and local, anywhere, at all times.”

05 Give the fleet a model that never leaves the box

Everything so far routes to whatever cloud agent you point herdr at. The box can also hold its own model, for the work you'd rather not ship to anyone's API: llama.cpp's llama-server, serving an OpenAI-compatible endpoint on loopback, for any seat to call. One card, one model, no round trip.

The box runs
install, Arch (AUR)
# NVIDIA
yay -S llama.cpp-cuda
# AMD
yay -S llama.cpp-hip
install, Debian / Ubuntu
# no apt package; grab a prebuilt release instead. The Vulkan build runs on
# either vendor with no separate GPU toolkit to install:
curl -fsSLO https://github.com/ggml-org/llama.cpp/releases/latest/download/llama-b<build>-bin-ubuntu-vulkan-x64.tar.gz
tar xzf llama-b<build>-bin-ubuntu-vulkan-x64.tar.gz -C ~/.local/llama.cpp
# on AMD, swap "vulkan" for "rocm-<rocm-ver>" in that filename for the native ROCm build
install, macOS
brew install llama.cpp   # Metal backend, built in; no separate GPU toolkit
install, Windows
# no winget package; grab a prebuilt release instead
# NVIDIA:
curl -fsSLO https://github.com/ggml-org/llama.cpp/releases/latest/download/llama-b<build>-bin-win-cuda-<cuda-ver>-x64.zip
# AMD:
curl -fsSLO https://github.com/ggml-org/llama.cpp/releases/latest/download/llama-b<build>-bin-win-rocm-<rocm-ver>-x64.zip
Expand-Archive llama-b<build>-bin-win-*-x64.zip -DestinationPath $HOME\llama.cpp

The model is the same file everywhere. This article's own box runs Qwen3.8-27B at Unsloth's UD-Q4_K_XL dynamic quant: 17.6 GB, which leaves working room on a 24 GB card, and their mixed-precision recipe keeps the quality well above a flat Q4 quant of the same size. Smaller card: UD-Q3_K_XL (13.1 GB) is the equivalent step down, same tradeoff a smaller model would make on a 16 GB card.

pull the model, once
hf download unsloth/Qwen3.8-27B-GGUF Qwen3.8-27B-UD-Q4_K_XL.gguf \
  --local-dir ~/models
run it
llama-server \
  -m ~/models/Qwen3.8-27B-UD-Q4_K_XL.gguf \
  -ngl 999 -fa \
  -c 32768 --cache-type-k q8_0 --cache-type-v q8_0 \
  --host 127.0.0.1 --port 8080

-ngl 999 offloads every layer to the card; -fa turns on flash attention, which this quant and this card both support. Quantizing the KV cache (q8_0 on both sides) stretches what's left of the 24 GB card past the model weights; 32k is a starting point, not a measured ceiling the way it was for the smaller model this article ran before, so watch VRAM the first time it fills up and back off -c, or drop to UD-Q3_K_XL, if the card is also doing anything else. Point any seat at http://127.0.0.1:8080/v1 and it's just another OpenAI-compatible endpoint, indistinguishable to the agent from a cloud one, except that nothing it sees leaves the box.

For a human-facing chat client against that same endpoint rather than curling it by hand: Open WebUI is the cross-platform default, a browser UI that speaks the same OpenAI-compatible API llama-server already serves. On a Mac, Local Studio is the native alternative: a signed app with the same OpenAI-compatible proxy in front of llama.cpp, plus GPU/runtime monitoring and its own agent workbench, built specifically around backends like this one.

~/.config/systemd/user/llama-server.service
[Unit]
Description=local model server
After=network-online.target

[Service]
ExecStart=%h/.local/llama.cpp/llama-server -m %h/models/Qwen3.8-27B-UD-Q4_K_XL.gguf -ngl 999 -fa -c 32768 --cache-type-k q8_0 --cache-type-v q8_0 --host 127.0.0.1 --port 8080
Restart=on-failure

[Install]
WantedBy=default.target

Same idea as the fleet's own service, one launchd plist in ~/Library/LaunchAgents running the command above with KeepAlive set, loaded with launchctl load.

Same idea as the fleet's own service: wrap the command above as a service with NSSM, or a login-triggered Task Scheduler task, so it's back after a reboot without a login.

The traps

Herdr

A remote attach can start a server, not just find one

herdr --remote doesn't refuse to run when herdr-fleet is down on the box: it can spin up a fresh, unmanaged session on the spot, and it looks exactly like the real thing until you close the laptop and it dies with you. That's what makes this trap sneaky: it doesn't fail loud, it fails quiet, weeks later, when the systemd unit crashed and nobody noticed because every attach kept working. Check the unit's status on the box before trusting what's in front of you.

Herdr

The state label is a view, not the truth

The idle / working / blocked / done badge is herdr reading the pane, not the agent's ground truth. A done agent can still be holding a port or a file lock, and an idle one can have left a server running. Before you reuse a pane or the box, check the actual process: the badge tells you what the terminal last looked like, not what the box is doing.

Herdr

HERDR_ENV surprises a tool

Managed panes carry HERDR_ENV=1 so an agent can tell it is under herdr. Occasionally a tool that sniffs its environment misbehaves when that variable is present. If something refuses to run inside a pane but works in a bare shell, env | grep HERDR and clear the variable for that one command rather than assuming the box is broken.

Lingering

A reboot is not a clean slate

enable-linger plus Restart=always means the fleet, and the side effects of every agent it ran, survive a reboot. A box that comes back “fresh” after a restart may still be holding an old port, a half-finished branch or a stale session. Lingering is a feature until the day you wanted a clean start and got a checkpoint instead.

Relay

An empty --allow list means anyone with the address gets in

Leave tailcat serve's --allow unset and every client is allowed: the address blob alone is the only gate. Set it to your client key's public key from the first systemctl --user enable --now, not after. Rotating the server key with --fixed-region --force leaves --allow untouched, since it's keyed on the client's public key, not the server's; no need to touch it on a server-side rotation.

Relay

The auth-free SSH service skips the layer you just pinned

tailcat serve can run no-auth-ssh, an SSH-shaped service where the tunnel itself supplies identity instead of a real key exchange. It's convenient and it's exactly the wrong tool here: it never touches your box's real sshd, so it never negotiates the post-quantum KexAlgorithms you just pinned. Serve the actual port 22 and let the real SSH protocol run end to end, or the PQ pin is decorative.

Handoff

--handoff isn't for moving between devices

It's tempting to reach for herdr --handoff to move your live attach from one machine to another. Don't. Paired with --remote, it exists to survive a remote server update or restart without dropping the attach, not to hop devices. Moving devices needs nothing: the fleet's state lives on the box, not on whichever laptop is currently looking at it, so a plain herdr --remote devbox --session main from the new device is the entire move.

Local LLM

Naming a model can evict the one that's running

Run llama-server with more than one model registered behind a --models-max cap, and asking for a model by name in any request (not just a completion: a bare /slots?model= or /metrics?model= counts too) can autoload it, evicting whatever was resident and killing its generation mid-stream. /v1/models, /props and /health are the only endpoints guaranteed not to touch what's loaded. If a seat might be mid-generation, ask before you poll.

Hand it to an assistant

If you'd rather not type this yourself, copy the prompt below and hand it to Claude Code, Codex, or whatever agent you point herdr at. It assumes the box from part one, a Linux box you can SSH into, and sets up the fleet, the seats, the relayed PQ-SSH transport, and, if you give it a GPU, a local model, in one go.

the prompt
Set up a persistent, remotely-attachable agent fleet on a Linux dev box, plus
a relayed SSH transport with post-quantum key exchange so it is reachable
from any network. I'll fill the placeholders.

MY SETUP
- Box: a Linux machine I can SSH into as user <dev>. It stays on; it is a
  server, not a laptop.
- Existing SSH entry in ~/.ssh/config:
    Host devbox
        HostName <lan-ip>
        User <dev>
        DynamicForward 1080
        ServerAliveInterval 30
        ServerAliveCountMax 3
        ExitOnForwardFailure yes
  (drop any of the last three lines that aren't actually there; don't add
  ones I don't have)
- A persistent SOCKS tunnel service already dials this host in the
  background (autossh under a user service, or the launchd/Task
  equivalent). Name: <service name>.
- herdr is installed on the box (verify with: herdr --version). If it is not
  installed, stop and ask me.
- tailcat is installed on the box and on my laptop (verify with:
  tailcat version). If it is not installed, stop and ask me.
- GPU on the box (optional, only needed for step 4): <card and VRAM, e.g.
  "24GB">. Leave this blank and skip step 4 entirely if I don't fill it in.

DO THIS
1. FLEET. Create ~/.config/systemd/user/herdr-fleet.service:
     [Unit]
     Description=Herdr agent fleet (headless)
     After=network-online.target
     Wants=network-online.target

     [Service]
     Environment=HERDR_SESSION=main
     ExecStart=%h/.local/bin/herdr server
     Restart=always
     RestartSec=10

     [Install]
     WantedBy=default.target
   (Fix the ExecStart path if herdr lives elsewhere: `command -v herdr`.)
   Then run:
     systemctl --user daemon-reload
     systemctl --user enable --now herdr-fleet.service
     sudo loginctl enable-linger "$(whoami)"
   Confirm with `systemctl --user status herdr-fleet.service --no-pager`
   that it is active (running).

2. SEATS. Create docs/agents/ on the box with eight seat files, one per role,
   each following this five-part skeleton (keep the shape identical). Base
   the role-specific parts on my actual project, not placeholders: read my
   repo's README/docs and, if present, ~/.claude/CLAUDE.md and
   ~/.claude/projects/*/memory/ for what's already true about the product
   and how I work. Ask me only for what you can't find there.
     You are the <Seat> for <product>. Your north star is <one sentence>.

     You own:
     - <the surface you are accountable for>
     - <the decisions you make without asking>
     - <the recurring cadence you run>

     Facts you never re-derive (read the file, don't guess):
     - <the seams, where this seat meets the others>

     You never:
     - <the hard boundaries; what is out of scope>

     Good output here looks like:
     - <the concrete shape of a finished piece of work>
   The eight seats: chief-of-staff, research-lead, content-lead,
   engineering-lead, design-lead, platform-ops, finance, and one domain seat
   named for my product. Every seat prepares; none of them execute: no seat
   pushes, deploys, publishes, sends, or moves money on its own.

3. RELAY + PQ SSH. On the box:
     tailcat genkey --key=default --fixed-region
     # -> prints an address blob; note it
   On my laptop:
     tailcat genkey --client --key=client-default
     # -> prints a public key; tell it to me, don't guess it
   On the box, lock the relay to that one client key from the start:
     ~/.config/systemd/user/tailcat-ssh.service:
       [Unit]
       Description=tailcat relay for SSH access
       After=network-online.target
       Wants=network-online.target

       [Service]
       ExecStart=%h/.local/bin/tailcat serve --key=default --allow=nodekey:<client-pubkey> 22
       Restart=on-failure
       RestartSec=5

       [Install]
       WantedBy=default.target
     systemctl --user daemon-reload
     systemctl --user enable --now tailcat-ssh.service
     tailcat resolve <address-blob>   # a durable, self-contained token
   On the box (needs root; hand me the exact command instead of running it
   yourself if you don't have sudo):
     echo 'KexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512' \
       | sudo tee /etc/ssh/sshd_config.d/99-pq-kex.conf
     sudo sshd -t
     sudo systemctl reload sshd
   Rewrite the devbox entry in ~/.ssh/config to dial through the relay.
   Replace only the HostName line with ProxyCommand; keep every other line
   (DynamicForward, and ServerAliveInterval/ServerAliveCountMax/
   ExitOnForwardFailure if I have them) exactly as it was:
     Host devbox
         ProxyCommand tailcat <resolved-address-blob> 22
         User <dev>
         DynamicForward 1080
         ServerAliveInterval 30
         ServerAliveCountMax 3
         ExitOnForwardFailure yes
   If my persistent SOCKS tunnel service is running, restart it now so it
   reconnects through the relay instead of the stale HostName, then tell me
   to rerun its own verification (curl --socks5-hostname) before I trust
   the browser side again.

4. LOCAL MODEL. Skip this step entirely if I left GPU blank above.
     Install llama.cpp (the llama-server binary) for the box's OS and GPU
     vendor via its own install docs; confirm after with:
       llama-server --version
     Pull the model. If I gave a GPU at or above 24GB, use UD-Q4_K_XL; below
     that, use UD-Q3_K_XL instead and tell me you made that substitution:
        hf download unsloth/Qwen3.8-27B-GGUF \
          Qwen3.8-27B-UD-Q4_K_XL.gguf --local-dir ~/models
     Create ~/.config/systemd/user/llama-server.service (adjust the quant
     filename to match whichever one you pulled):
       [Unit]
       Description=local model server
       After=network-online.target

       [Service]
       ExecStart=%h/.local/llama.cpp/llama-server -m %h/models/Qwen3.8-27B-UD-Q4_K_XL.gguf -ngl 999 -fa -c 32768 --cache-type-k q8_0 --cache-type-v q8_0 --host 127.0.0.1 --port 8080
       Restart=on-failure

       [Install]
       WantedBy=default.target
     systemctl --user daemon-reload
     systemctl --user enable --now llama-server.service
     Confirm with: curl -s http://127.0.0.1:8080/v1/models
     Point one seat's model config at http://127.0.0.1:8080/v1 so at least
     one seat runs fully local; leave the rest on whatever cloud agent they
     already use.

CONSTRAINTS
- Do not touch anything outside the fleet unit, docs/agents/, the devbox SSH
  entry, the tailcat relay unit, the sshd_config.d drop-in, ~/models and the
  llama-server unit (only if step 4 runs), and restarting (never editing or
  removing) my existing SOCKS tunnel service. No other services, no repo
  changes.
- Never read or print the contents of a tailcat private key file
  (~/.config/tailcat/keys/*.private.json), an SSH private key, or any other
  secret. Public address blobs and public keys are fine to print.
- If herdr or tailcat is missing, or the box is not the machine you think it
  is, STOP and report before doing anything destructive.
- You will not have sudo. For the sshd_config.d write, validate, and reload
  steps, hand me the exact commands to run myself rather than attempting them.

KNOWN TRAPS (check these, in order, if a step fails)
- `herdr --remote devbox --session main` can start a fresh, unmanaged session
  if herdr-fleet isn't running on the box, instead of failing: it looks fine
  until the laptop closes. Check the unit's status before trusting an attach.
- Leaving tailcat's --allow unset lets any client with the address blob in.
  Set it to the client key's public key from the first enable --now, not
  after. A server-key rotation with --fixed-region --force leaves --allow
  untouched: it's keyed on the client's public key, not the server's.
- tailcat's `no-auth-ssh` service never touches the box's real sshd, so it
  never negotiates the post-quantum KexAlgorithms. Serve plain port 22.
- The idle/working/blocked/done badge is herdr's heuristic read of the pane,
  not ground truth: verify a pane's real state (ps, ports) before reusing it.
- Managed panes set HERDR_ENV=1. If a tool misbehaves inside a pane but works
  in a bare shell, that variable is the likely cause.
- enable-linger + Restart=always means a reboot does not clear state; an
  old port or branch survives. Check before assuming a fresh box.
- `--handoff` is not for moving between devices: pass on it unless you
  specifically need to survive a remote herdr update mid-attach.
- Rewriting the devbox Host block from scratch can silently drop
  ServerAliveInterval, ServerAliveCountMax or ExitOnForwardFailure if they
  were already there. Change the connection line only. And my persistent
  SOCKS service won't notice the new ProxyCommand until it reconnects: a
  working `herdr` attach afterward does NOT prove that service is healthy,
  check it separately.
- If that persistent SOCKS service picks between a LAN route and this relay
  on its own, check what it bases that choice on before trusting it. Reading
  this machine's own interface prefix and comparing it to the LAN's is not a
  location check: plenty of routers hand out the same default prefix, so it
  can match on a network I'm only visiting and dial a LAN IP that isn't
  there, hanging with nothing logged more specific than a stalled connect.
  It should probe the LAN endpoint directly instead (e.g.
  `nc -z -G 2 <lan-ip> 22`, bounded to a couple seconds) and route on
  whether that succeeds, not on what my own address happens to look like.
- If step 4 runs: this quant is a 27B vision-capable model, not the smaller
  one it may replace on an existing box, and it eats VRAM fast. If
  llama-server fails to start or the card OOMs, drop to UD-Q3_K_XL before
  lowering -c: the smaller quant buys back more headroom per GB than
  trimming context does.

HOW TO REPORT
Show me, in order:
  - the output of `systemctl --user status herdr-fleet.service --no-pager`
  - `ls docs/agents/` (should list all eight seat files)
  - the output of `systemctl --user status tailcat-ssh.service --no-pager`
  - the resolved address blob and the client's public key (never the private
    key files themselves)
  - confirmation that `sudo sshd -t` passed and `KexAlgorithms` is active
  - the final devbox block of ~/.ssh/config, diffed against what it was
    before this prompt, so I can see nothing but HostName/ProxyCommand moved
  - confirmation the SOCKS tunnel service was restarted and its own
    verification still passes
  - if step 4 ran: the output of `curl -s http://127.0.0.1:8080/v1/models`,
    which quant it pulled, and which seat (if any) now points at it
  - the exact command to attach from the laptop:
        herdr --remote devbox --session main
And list every trap that fired and how you resolved it.

Removing it

Tear the fleet down when the box is done being a server.

back it out
# stop and disable the fleet (this tears down the panes and their agents)
systemctl --user disable --now herdr-fleet.service
rm ~/.config/systemd/user/herdr-fleet.service
systemctl --user daemon-reload

# remove the seats, if you want the box clean
rm -rf docs/agents

# stop and disable the relay
systemctl --user disable --now tailcat-ssh.service
rm ~/.config/systemd/user/tailcat-ssh.service
systemctl --user daemon-reload
tailcat genkey --delete --key=default   # and, on the laptop: --key=client-default

# drop the PQ pin (needs root)
sudo rm /etc/ssh/sshd_config.d/99-pq-kex.conf
sudo systemctl reload sshd

# stop and disable the local model server, if you added one
systemctl --user disable --now llama-server.service
rm ~/.config/systemd/user/llama-server.service ~/models/Qwen3.8-27B-UD-*.gguf
systemctl --user daemon-reload

# swap ProxyCommand back for the LAN HostName in ~/.ssh/config, leaving
# DynamicForward and any ServerAlive*/ExitOnForwardFailure lines untouched
# (or delete the whole entry, if the box is going away entirely), then
# restart part one's SOCKS tunnel service so it reconnects over the LAN

Nothing here is destructive to your code: the repos and the seat files are plain files on disk, and the tunnel's side of the teardown is part one's job. The only thing that does not come back is the live pane state, which is a view: the source of truth was always on disk.


Written to be skimmed by a human and executed by an agent. The herdr flags are from herdr --help (v0.8.2); the tailcat flags are from tailcat --help (v0.4.0); the llama-server flags are from its own --help; the seats generalize a working eight-seat roster; the SSH steps are standard. If a flag has moved in your version of any of them, its own --help is authoritative. The tunnel this builds on is The Any-Port Dev Tunnel.