Keeping Hermes runs contained
I’m using Hermes to do the implementation work here, and I don’t want it wandering around with the keys to everything. I want the blast radius contained.
A lot of coding tools make the wrong thing too easy. You install one, hand it a shell, give it credentials it shouldn’t have, and now it can do anything you can do — git push --force, rm -rf, talk to your cloud account, walk into your dotfiles. One bad turn and you lose an evening.
Why I keep it boxed in
I don’t need the model roaming around my machine. I need one job done in one place, then a branch back. That’s the shape of it.
The work breaks into three parts for me: orchestration, implementation, and review. Hermes does the implementation inside a container. I stay outside it for the rest.
The container boundary
The implementation job runs claude -p inside a container. Inside the container live git, node, the package manager, the project’s test runners — whatever the repo already depends on — plus the project repo itself, mounted in. What’s not inside: host SSH keys, credentials for unrelated services, my home directory, any other project on the machine.
The image is short and boring on purpose: a base Node image, git and ripgrep installed, Claude Code available, and /work as the working directory.
The repo gets bind-mounted into /work at run time. No SSH agent forwarded, no host config mounted in.
This boundary is the thing doing the work, not the model behind it. The model can be excellent or middling on any given night; the container doesn’t care. If the run goes sideways — wrong files edited, infinite loop, weird shell incantation — the damage is bounded by what the container can reach. Worst case I kill the run, throw away the branch, and I’ve lost the time the run took plus whatever the worker did over the network.
That last part is a real caveat. A container boundary isn’t an airtight boundary. The worker still has outbound network access so it can install packages and pull dependencies, and I haven’t locked egress down to a strict allowlist. So “bounded” here means bounded to the repo, the tools in the image, and whatever the network allows — a much smaller surface than my laptop, not zero surface.
I’ve seen people argue this is paranoid. I don’t think it is, but the cost isn’t free either. The image needs occasional rebuilds when a base layer gets a security update, dependencies drift and need pinning or bumping, and if I ever do want to lock egress down properly that’s a network-policy project of its own. It’s an ongoing maintenance line, not a one-time setup. The cost of not having any boundary at all is every future agent run sharing your laptop’s permissions, and I’d rather pay the maintenance.
The container itself is ephemeral — each run starts fresh and exits — but the image and the mount layout are persistent. That’s the part that needs looking after.
Task files as the unit of work
Each run starts from a task file. A task file is a markdown document that describes one job: what to build, which files are in scope, what done looks like, and any constraints the worker needs to respect. The orchestrator writes it. The container reads it.
This sounds bureaucratic and it isn’t. Writing the task file is the design step. By the time I’ve described the job clearly enough for a worker to do it, I usually know whether the job is a good idea, whether it’s the right size, and whether it has hidden dependencies. A lot of the design work happens here, before any code is generated.
Task files also make the work auditable. Six weeks from now, when I’m wondering why a particular module looks the way it does, the task file is sitting there with the original framing. There’s no hidden context lost in a chat window I closed.
Review as a separate check
The worker hands back a branch. It does not merge. The review pass is a separate step, run with a different prompt and a different intent: not “build this”, but “is this safe to keep”. Review looks at the diff, checks the task file’s done-criteria, and either approves or rejects.
I treat review as real work, not a rubber stamp. If review can’t tell what the diff is doing, the diff is wrong. If review finds something the task file didn’t anticipate, the task file gets a note and the next run is better-shaped. Review is also where I catch the implementation worker doing something clever I didn’t ask for.
When review approves, the branch gets merged. If a remote exists, the merge gets pushed. Push failures are a warning, not a failure of the run — the work is committed locally either way, and I’d rather a flaky network not poison an otherwise-good result.
What this gets right
The thing I notice most is that the failure mode is boring. A bad run produces a bad branch, the review pass throws it out, and I move on. There’s no half-edited working tree on my laptop. There’s no commit hanging off main that I have to revert. The “what did it touch” question gets much smaller, because the container’s reach is much smaller than my user account’s.
The other thing is that orchestration stops competing with implementation for attention. The orchestrator’s job is to pick what to run, write the task file, and decide what to do with the result. It doesn’t also need to be a great coder. The implementation worker is a competent coder that doesn’t need to make scheduling decisions. Each side gets to be good at its own thing.
For what it’s worth, this blog was scaffolded by a run of this workflow. That’s an anecdote, not evidence — a blog scaffold is an easy task and one data point doesn’t prove anything — but it’s why I happen to be writing the post inside the thing it describes.
Where it still breaks
It has rough edges. A few still bug me.
Long tasks are hard to size. Anything that takes the worker more than a couple of hours starts to feel like a project, and project-sized work doesn’t fit cleanly into a single task file. I haven’t found a clean answer beyond “split it”, which sometimes means writing three task files instead of one and accepting the overhead.
Cross-repo work is awkward. The container is mounted on one project. If a task naturally spans two repos, I either widen the mount and lose some of the boundary, or split the work along repo lines and lose some of the coherence. Neither is great. For now I live with it by keeping cross-repo work out of the overnight queue.
Review fatigue is real too. If I queue ten task files overnight and wake up to ten review passes, the tenth review is not as careful as the first. I’ve started capping the queue at what I can review properly the next morning, which is fewer runs than I’d like.
Closing
This isn’t magic and it isn’t autonomy. It’s just a cleaner split: one thing builds, one thing checks, one thing decides what runs next. The line around the container keeps the risky bits in one place. Task files keep the job explicit. Review keeps the merge honest.
Breaking the job up this way has worked better for me. That’s about it.