nateparker.dev

← Writing

Herdr and the overnight homelab

Coding agents are useful right up until you close your laptop. Here's how I gave mine somewhere to live, and the guardrails I put around them first.

7 min readagentshomelabsecuritytooling

The first genuinely useful thing an AI coding agent did for me took about forty minutes, and I watched roughly thirty-five of them. Not because I needed to — because the session lived in a terminal on my laptop, and my laptop was going to sleep the moment I stopped touching it.

That’s a strange way to use a tool whose main advantage is that it doesn’t get bored. The agent was perfectly happy to grind through a migration for an hour. I was the bottleneck, sitting there as a very expensive uninterruptible power supply.

So I moved the agents off my laptop. This is what that looks like now.

The actual problem

The problem isn’t that agents are slow. It’s that an agent session is a terminal session, and terminal sessions are tied to the machine and the process tree that started them. Close the lid, lose the network, restart for an OS update, and the work is gone — not paused, gone. Whatever context the agent had built up goes with it.

You can paper over this with tmux and SSH, and for a long time I did. But that gets awkward once you have more than one agent running, across more than one project, on more than one machine. You end up with a mental map of which pane on which host is doing what, and no way to tell at a glance which of them is working, which is blocked waiting on you, and which has been idle for two hours because it finished and you didn’t notice.

That last part is the real cost. Unattended agents are only useful if you can tell when they stop being unattended.

What Herdr does

Herdr is a background server that owns the terminal sessions your agents run in. That’s the whole idea, and I mean that as a compliment — it’s a small idea implemented thoroughly.

The parts that matter to me:

Sessions outlive the client. The server keeps running. Close the laptop, drop off the network, reboot the machine — Herdr brings the layout back and the agents pick up where they were. This is the feature. Everything else is convenience on top of it.

It reads the panes. Herdr watches the terminal output and marks each agent as working, blocked, or idle. So instead of cycling through panes looking for a prompt waiting on input, you get a list where the two that need you are obvious.

It doesn’t wrap the agent. Herdr owns the terminal, not the tool inside it. It works with the agent CLIs as they ship, which means I’m not waiting on Herdr to add support for a flag that Claude Code shipped last week. Given how fast these tools move, that architectural choice is worth more than any feature list.

Multiple machines, one view. Connect hosts over SSH and manage the workspaces together. This is what made the homelab worth pointing at the problem.

The homelab side

My homelab predates all of this by about a decade. It started as somewhere to run Security Onion and Splunk and break things without consequences, and it has slowly become the place where long-running work lives.

The current shape of it: an M4 Mac Mini running OrbStack, with services split into Docker Compose stacks grouped by function rather than one enormous file. A Synology NAS provides bulk storage over NFS. Caddy reverse-proxies everything to .lan hostnames so I’m not memorizing port numbers. Telegraf feeds host and container metrics into InfluxDB, and Grafana draws them.

Two details that took a while to get right, both about ordering:

Telegraf runs natively on macOS rather than in a container. A containerized Telegraf only sees the OrbStack Linux VM’s /proc, so it cheerfully reports the VM’s CPU and memory as if they were the host’s. The metrics look plausible and are wrong, which is the worst category of monitoring bug.

The NAS mount has a sentinel file. A launchd agent mounts it at boot, retrying until the NAS is reachable, and writes a marker file into the mount. The deploy script refuses to start storage-dependent stacks unless that marker is present. Without it, a stack that boots faster than the network comes up against an empty directory and helpfully starts filling it — on the local disk, under the mountpoint, invisible the moment the real mount lands on top.

None of it is exposed to the internet. It’s reachable on the LAN or over the home VPN, and that’s deliberate: unauthenticated internal tools and public addresses don’t belong in the same sentence. The agents run on that host, not on my laptop, and I attach to them from wherever I am.

The shift in how I work is bigger than it sounds. Agent work stopped being something I supervise and became something I queue. I hand off a few tasks before dinner — a dependency upgrade with a long test suite, a refactor nobody wants to do by hand, a first pass at test coverage for a module that never had any — and look at the results later. Some of it is good. Some of it gets thrown away. The throwing away is cheap, which is the point.

Guardrails first

I do application security for a living, so let me be direct about this: an agent running unattended on a machine that can reach your network is a piece of automation with broad permissions and non-deterministic behavior. That’s not a reason to avoid it. It’s a reason to scope it.

What I actually do:

Isolation per task. Every agent gets its own git worktree and its own container. It cannot see the other agents’ work, and it cannot reach anything on the network it wasn’t explicitly given. Blast radius is the only security control that reliably survives contact with a tool you don’t fully control.

No credentials in the environment. The agents don’t hold production secrets, cloud keys, or anything that would let them touch a real system. If a task genuinely needs a credential, that’s a signal the task shouldn’t be unattended.

Everything lands in a pull request. Nothing an agent writes goes to a default branch. It goes through the same review as code I write, running the same CI — which for my Rails projects means Brakeman and bundler-audit, and for Phoenix means Sobelow. An agent introducing a vulnerability is not meaningfully different from me introducing one at 11pm, and the same controls catch both.

Read the diff, not the summary. Agents are good at writing confident summaries of work they did not quite do. The summary is a hypothesis. The diff is the evidence.

None of this is exotic. It’s the same reasoning I’d apply to any CI runner with repo write access — which is exactly what this is, with a less predictable author.

What works, and what doesn’t

Works well: mechanical refactors with good test coverage, dependency upgrades where the failure mode is a red build, writing tests for existing behavior, and first drafts of documentation. Tasks where the definition of done is machine-checkable and the cost of a bad attempt is a discarded branch.

Works badly: anything requiring judgment about why the code is the way it is. My day job is a healthcare platform with years of accumulated decisions, and a lot of those decisions look wrong until you know which clinical workflow or regulatory requirement they’re serving. An agent will confidently simplify something load-bearing. Overnight autonomy makes that worse, because nobody’s watching when it happens.

The honest summary: this setup roughly doubles the amount of low-judgment work I get through, and does approximately nothing for the work that’s actually hard. That’s still a good trade. The low-judgment work was the part I resented.

Where it’s going

I’m increasingly interested in agents that coordinate with each other — Herdr exposes the same CLI to agents that it does to me, which makes that possible in a way that feels more like a build system than a magic trick. I haven’t pushed on it yet.

Mostly, though, I’ve stopped thinking of agents as a thing I sit down to use. They’re background jobs now. They have a place to run, a queue, and a review process. That’s a less exciting framing than the demos suggest, and it’s the framing that made them actually useful.

What I’m building next

The missing layer is something I can message. Not another dashboard — a single orchestrator I hand a task to, that decides how many agents it needs, spawns them, and owns the plan while they work. Herdr already exposes the same CLI to agents that it does to me, so the primitive is there; what doesn’t exist yet is the thing holding the intent.

The other half is checking in without watching. A heartbeat on a cron schedule that reports what’s running, what’s blocked, and what finished while I wasn’t looking — so “how’s it going” gets answered on a schedule instead of requiring me to go ask. Push, not poll. That’s the difference between background work and work you’re still supervising, just more slowly.

If that sounds like a supervision tree with a chat interface, that’s about right. I’d rather rediscover OTP than invent something worse.

I’m also going to put a second model through the same setup — GPT Astra, alongside Claude Code, on identical tasks. Running two models against the same work is the cheapest way I know to find the places where one of them is confidently wrong, and confidently wrong is the failure mode that unattended execution is worst at catching.