[{"data":1,"prerenderedAt":305},["ShallowReactive",2],{"blog:\u002Fblog\u002F2026-07-06-operator-loop-for-unattended-agents":3},{"id":4,"title":5,"body":6,"date":295,"description":296,"extension":297,"meta":298,"navigation":299,"path":300,"seo":301,"stem":302,"tag":303,"__hash__":304},"blog\u002Fblog\u002F2026-07-06-operator-loop-for-unattended-agents.md","An operator's loop for running coding agents unattended",{"type":7,"value":8,"toc":288},"minimark",[9,13,25,28,33,36,39,42,45,49,52,142,145,149,152,172,179,182,186,189,192,256,259,262,266,269,281,284],[10,11,5],"h1",{"id":12},"an-operators-loop-for-running-coding-agents-unattended",[14,15,16,17,24],"p",{},"I built ",[18,19,23],"a",{"href":20,"rel":21},"https:\u002F\u002Fgithub.com\u002Ftcashel\u002Fforge",[22],"nofollow","forge"," over about five weeks of free time: a harness for running coding agents unattended. You hand it a spec, walk away, and come back to a draft PR that a second model has already reviewed. It's roughly 24k lines of TypeScript on Bun, still pre-release, and no longer just mine: a few people at work use it now, and the PR-review piece has been pulled out into work's own tooling.",[14,26,27],{},"The thing I keep coming back to is that none of the individual pieces are clever. A git worktree is just git. \"Two critics\" is two model calls. A synthesizer merges text. What makes the system useful is the composition of those boring pieces into a single loop you only touch once. This post walks through how the loop fits together, and the one place it is still weak.",[29,30,32],"h2",{"id":31},"what-actually-bites-you-when-an-agent-runs-unattended","What actually bites you when an agent runs unattended",[14,34,35],{},"Two failure modes cause nearly all of the pain, and neither of them is the model writing bad code.",[14,37,38],{},"The first is the vague spec. A vague spec doesn't fail loudly: the agent builds the wrong thing, correctly, and you don't find out until you're already reading the diff. You can't lint your way out of bad requirements.",[14,40,41],{},"The second is trusting the result. An agent runs, declares victory, and opens a PR praising its own change. If the same agent that wrote the change also decides the change is good, you haven't removed the review work; you've moved it onto yourself, after the fact, which defeats the reason for walking away in the first place. It's the same separation-of-duties argument auditors make: the person who does the work shouldn't be the one who signs off on it. Don't let an agent grade its own homework.",[14,43,44],{},"So the loop is built around those two problems specifically: review the input before it runs, and never let the writer be the reviewer.",[29,46,48],{"id":47},"the-shape-of-the-loop","The shape of the loop",[14,50,51],{},"The human touches this once, at the spec. Everything after that runs on its own.",[53,54,59],"pre",{"className":55,"code":56,"language":57,"meta":58,"style":58},"language-mermaid shiki shiki-themes github-dark","flowchart TD\n    A[\"I write a spec\"] --> B{\"Spec review\"}\n    B --> C1[\"Critic A · model 1\"]\n    B --> C2[\"Critic B · model 2\"]\n    C1 --> S[\"Synthesizer\u003Cbr\u002F>merges both critiques\u003Cbr\u002F>into one improved spec\"]\n    C2 --> S\n    S --> H{{\"I approve the spec\u003Cbr\u002F>(the only human gate)\"}}\n    H --> I[\"Implementer builds\u003Cbr\u002F>in an isolated git worktree\"]\n    I --> PR[\"Draft PR\"]\n    PR --> R[\"Reviewer · a different model\"]\n    R --> F[\"Implementer fixes\u003Cbr\u002F>against the review\"]\n    F -->|\"up to ~2 passes\"| R\n    R --> Done[\"PR ready for me to merge\"]\n","mermaid","",[60,61,62,70,76,82,88,94,100,106,112,118,124,130,136],"code",{"__ignoreMap":58},[63,64,67],"span",{"class":65,"line":66},"line",1,[63,68,69],{},"flowchart TD\n",[63,71,73],{"class":65,"line":72},2,[63,74,75],{},"    A[\"I write a spec\"] --> B{\"Spec review\"}\n",[63,77,79],{"class":65,"line":78},3,[63,80,81],{},"    B --> C1[\"Critic A · model 1\"]\n",[63,83,85],{"class":65,"line":84},4,[63,86,87],{},"    B --> C2[\"Critic B · model 2\"]\n",[63,89,91],{"class":65,"line":90},5,[63,92,93],{},"    C1 --> S[\"Synthesizer\u003Cbr\u002F>merges both critiques\u003Cbr\u002F>into one improved spec\"]\n",[63,95,97],{"class":65,"line":96},6,[63,98,99],{},"    C2 --> S\n",[63,101,103],{"class":65,"line":102},7,[63,104,105],{},"    S --> H{{\"I approve the spec\u003Cbr\u002F>(the only human gate)\"}}\n",[63,107,109],{"class":65,"line":108},8,[63,110,111],{},"    H --> I[\"Implementer builds\u003Cbr\u002F>in an isolated git worktree\"]\n",[63,113,115],{"class":65,"line":114},9,[63,116,117],{},"    I --> PR[\"Draft PR\"]\n",[63,119,121],{"class":65,"line":120},10,[63,122,123],{},"    PR --> R[\"Reviewer · a different model\"]\n",[63,125,127],{"class":65,"line":126},11,[63,128,129],{},"    R --> F[\"Implementer fixes\u003Cbr\u002F>against the review\"]\n",[63,131,133],{"class":65,"line":132},12,[63,134,135],{},"    F -->|\"up to ~2 passes\"| R\n",[63,137,139],{"class":65,"line":138},13,[63,140,141],{},"    R --> Done[\"PR ready for me to merge\"]\n",[14,143,144],{},"Two details matter more than the diagram suggests. The spec review happens before any code exists, which is the cheapest place to catch a mistake. And the post-PR review is a separate step run by a different model than the one that wrote the code. By the time a PR reaches me it has been reviewed twice, once as a spec and once as a diff.",[29,146,148],{"id":147},"every-part-is-deliberately-boring","Every part is deliberately boring",[14,150,151],{},"If you pull the loop apart, there is no single impressive component:",[153,154,155,163,166,169],"ul",{},[156,157,158,159,162],"li",{},"The worktree is plain ",[60,160,161],{},"git worktree",". Each run gets its own branch and working directory.",[156,164,165],{},"The two critics are two model calls with different models behind them. They reliably catch different things: one flags the edge cases I left out, the other flags where I over-scoped the work. Different models have different blind spots.",[156,167,168],{},"The synthesizer reconciles those two critiques into one improved spec, so I approve a single sharpened version instead of refereeing a pile of conflicting notes.",[156,170,171],{},"The reviewer and the fixer are, again, just model calls in a short loop.",[14,173,174,175,178],{},"Most of those 24k lines aren't the agents at all. They're the unglamorous part: state that survives a crash, atomic writes so two runs can't corrupt shared files, and a safety machine that decides what's safe to clean up. That safety machine taught me the one rule I'd carry to any version of this: ground truth is the real system state, not your database. forge reads ",[60,176,177],{},"git worktree list"," to decide what exists, and the database is only annotation. A database drifts and git doesn't; the moment a harness trusts its own bookkeeping over reality, it starts deleting things it shouldn't.",[14,180,181],{},"The part I didn't expect is where the leverage concentrated. The highest-value place to spend attention turned out to be the input, not the output: a spec two models have already argued over is worth more than any amount of cleanup after the fact. The parts are commodity; the loop is what's worth building, because it puts your attention in exactly one place.",[29,183,185],{"id":184},"the-gap-full-agent-isolation","The gap: full agent isolation",[14,187,188],{},"This is the part I'm not happy with yet, and the reason forge is still a tool I watch rather than one I'd point at anything sensitive.",[14,190,191],{},"A worktree isolates the repository: the branch and the files for that run. It does not isolate the agent. The process still runs with my machine's access: my filesystem outside the repo, my network, my credentials. \"Isolated run\" today means \"isolated git state,\" not \"isolated blast radius.\"",[53,193,195],{"className":55,"code":194,"language":57,"meta":58,"style":58},"flowchart LR\n    subgraph today[\"Isolated today\"]\n      W[\"git worktree\u003Cbr\u002F>branch + files for this run\"]\n    end\n    subgraph gap[\"NOT isolated yet: the gap\"]\n      P[\"agent process\"]\n      N[\"network access\"]\n      M[\"rest of the machine\u003Cbr\u002F>+ my credentials\"]\n    end\n    W -.->|\"but the agent still reaches past it\"| P\n    P --- N\n    P --- M\n",[60,196,197,202,207,212,217,222,227,232,237,241,246,251],{"__ignoreMap":58},[63,198,199],{"class":65,"line":66},[63,200,201],{},"flowchart LR\n",[63,203,204],{"class":65,"line":72},[63,205,206],{},"    subgraph today[\"Isolated today\"]\n",[63,208,209],{"class":65,"line":78},[63,210,211],{},"      W[\"git worktree\u003Cbr\u002F>branch + files for this run\"]\n",[63,213,214],{"class":65,"line":84},[63,215,216],{},"    end\n",[63,218,219],{"class":65,"line":90},[63,220,221],{},"    subgraph gap[\"NOT isolated yet: the gap\"]\n",[63,223,224],{"class":65,"line":96},[63,225,226],{},"      P[\"agent process\"]\n",[63,228,229],{"class":65,"line":102},[63,230,231],{},"      N[\"network access\"]\n",[63,233,234],{"class":65,"line":108},[63,235,236],{},"      M[\"rest of the machine\u003Cbr\u002F>+ my credentials\"]\n",[63,238,239],{"class":65,"line":114},[63,240,216],{},[63,242,243],{"class":65,"line":120},[63,244,245],{},"    W -.->|\"but the agent still reaches past it\"| P\n",[63,247,248],{"class":65,"line":126},[63,249,250],{},"    P --- N\n",[63,252,253],{"class":65,"line":132},[63,254,255],{},"    P --- M\n",[14,257,258],{},"In the meantime I run the agents behind a fairly extensive set of hooks that intercept what they try to do and block the dangerous moves before they land. The hooks catch a lot, and I wouldn't run unattended without them. But hooks are guardrails rather than a boundary: they stop the mistakes I predicted, and a sandbox has to stop the ones I didn't.",[14,260,261],{},"To actually trust unattended runs, especially more than one at a time, the agent itself needs that boundary: a sandbox with scoped network and resource limits, so a run can't reach past its own task even if it tries. That's what I'm building now. Until it's there, \"walk away and trust it\" has an asterisk.",[29,263,265],{"id":264},"what-id-tell-someone-building-this-today","What I'd tell someone building this today",[14,267,268],{},"Three things, in order:",[270,271,272,275,278],"ol",{},[156,273,274],{},"Spend your first effort on the spec-review step, not the agent. Front-loading the review onto the input was the highest-leverage change I made, and the cheapest to build.",[156,276,277],{},"Make ground truth the real system state. Read git (or whatever the real source is) and treat your own database as a cache that can lie.",[156,279,280],{},"Don't ship unattended execution until isolation is real. Reviewing the work is the easy half; bounding what a run can touch is the half that decides whether you can actually leave. Hooks buy time, but they don't close the gap.",[14,282,283],{},"The loop is the interesting part. The isolation decides whether the loop is safe to walk away from, and I'm still building that.",[285,286,287],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":58,"searchDepth":78,"depth":78,"links":289},[290,291,292,293,294],{"id":31,"depth":72,"text":32},{"id":47,"depth":72,"text":48},{"id":147,"depth":72,"text":148},{"id":184,"depth":72,"text":185},{"id":264,"depth":72,"text":265},"2026-07-06","Review the spec before any code exists, never let the writer be the reviewer, and the isolation gap that's still open.","md",{},true,"\u002Fblog\u002F2026-07-06-operator-loop-for-unattended-agents",{"title":5,"description":296},"blog\u002F2026-07-06-operator-loop-for-unattended-agents","Agents","ulmIr8lbgz0g1uXS1grSc690yRyY7MLqW4eazdEaX4k",1786110827758]