blog
Loop engineering: stop prompting the agent, design the loop that prompts it
Building a loop for an agent now takes fifteen minutes. The hard part is putting something inside it that dares to say "no". The five moves of one pass, five ways a loop fails, and the real loop on this site, which is halfway there.
- Published
- Updated on
Building a loop for an agent now takes fifteen minutes: one cron line, one skill, one run command. The hard part is putting something inside it that dares to say "no".
In June 2026, three people named the same way of working within a single week. Peter Steinberger wrote that you should stop prompting coding agents and design loops that prompt them instead. Boris Cherny, who leads Claude Code, said he no longer prompts Claude; he has loops running that prompt Claude and work out what to do next. Addy Osmani named it and wrote it down on 7 Jun 2026: loop engineering is replacing yourself as the person who prompts the agent, then designing the system that does that job for you.
The loop sits one layer above the harness, and three verbs set it apart
These four layers stack on top of each other; none replaces another. The prompt handles the wording of a single turn. Context handles what is loaded into the window right now. The harness equips a single run: which tools, which actions are allowed, what counts as done. The loop handles the rest: how it runs again by itself, over and over.
The harness ends where one run finishes and then sits there waiting. Three verbs separate the loop from the harness:
- Runs on a schedule. The loop wakes itself up; nobody has to press a button.
- Spawns sub-agents. One turning loop splits into several child agents, one that writes code, one that only hunts for bugs.
- Feeds on its own output. Today's results are written to a file, and tomorrow morning it reads that file back and carries on.
The third verb turns it into a loop instead of a task that runs many times.
Each layer fails in a different way, and that is the real reason to separate them. Take one bug: the agent misreads a function's return value. At the prompt layer, it gives one wrong answer and the person sitting there corrects it on the spot. At the harness layer, the agent edits a file and finishes, and the diff sits there waiting for review. At the loop layer, that misreading is written into the state file, read back the next morning as a settled fact, and everything gets built on top of it. By the time someone looks, the wrong assumption has become load-bearing. The cost of a bug is proportional to the number of passes it survives before it is caught, and a loop is, by nature, a machine for maximizing the number of passes.
Five moves in one pass: discovery, handoff, verification, persistence, scheduling
Drop any one of the five and the loop either stops turning or spins in place.
| Move | What it does | Implemented with |
|---|---|---|
| Discovery | Finds the work for this pass on its own | A skill that reads CI, issues, new commits |
| Handoff | Hands out the work, each task in its own space | git worktree for each agent |
| Verification | Swaps in a different agent to say "no" | Sub-agent review, a different model |
| Persistence | Writes state outside the conversation | A markdown file or a board, committed |
| Scheduling | Makes it run the next pass | cron, GitHub Actions, automation |
Discovery sets the ceiling for the whole loop. If it only finds worthless work, the other four moves can be as polished as you like and they still serve nothing. The work-finding logic belongs in a named skill; do not paste a whole block of text into the crontab, because a skill file keeps getting edited while a block of text in a crontab just rots there.
Osmani lists five components: automations, worktrees, skills, connectors, sub-agents. I add a sixth, memory, meaning state that lives on disk. Memory differs from context: context is what the agent sees this pass and is wiped on refresh; memory lives across days. The agent forgets, the repo does not.
An agent grading its own work always gives itself high marks
This is where every loop dies. Ask an agent to evaluate what it just wrote and it praises it, with confidence, even when someone sitting next to it can see at a glance that the work is mediocre. Anthropic engineer Prithvi Rajasekaran ran into exactly that while building long-running agent applications.
The cause lies in the context. By then the window is packed with reasons why the code was written the way it was, so when the agent looks back at its output, it sees the chain of reasoning that led it there instead of the result.
Rewording the prompt does not fix it. Pushing the generator to be harder on itself is close to useless, while tuning an independent evaluator to be skeptical is much easier. Nobody can step outside their own point of view, but you can swap in a different agent, with different instructions, that looks at the code from zero.
# .claude/agents/reviewer.md
ROLE: Adversarial code reviewer.
ASSUME: this code is BROKEN until proven otherwise.
DO NOT praise. Find what fails.
CHECK, in order:
1. Does it run? (execute, don't read)
2. Tests: run them, paste real output.
3. Edge cases the author skipped.
4. Does behavior match the ticket?
USE Playwright MCP: open the page, click, screenshot, inspect the DOM.
Judge behavior, not intent.
VERDICT: PASS only if every check holds. Otherwise REJECT + list each reason.
Two details in that file matter more than the rest. The evaluator has to act, not just read. If it only reads, it judges "looks right" rather than "runs right"; connect it to Playwright MCP so it opens the page, clicks the buttons and takes screenshots, and the basis for its verdict shifts from "this JSX looks fine" to "I clicked the button, the page changed, here is the screenshot". Change the underlying model too, because the same model with new instructions usually keeps its old blind spots.
The stop condition should also go to a different model. The Claude Code docs describe /goal as "Claude keeps working across turns until the condition is met". After each turn, a small, fast model checks whether the condition has been met; if not, another turn runs instead of control going back to you. This is the maker-checker principle from banking, where the person who enters a large transfer and the person who approves it must be two different people, applied to the question "are we done yet".
/goal all tests in test/auth pass and the lint step is clean
The generator's skill decides what the loop can produce; the evaluator's skill decides what it refuses to let through. So the quality floor of the whole loop sits with the evaluator.
The loop on this site is only halfway there
The repo for nguyentrungkhuong.com runs a real loop. It is small, and I know exactly what it is missing.
The moves that exist: persistence is the .sources.yml files that sit next to each blog post (in July 2026 there were 10 files with 102 claims in total), where each claim records which number came from which source. Verification is check-sources.js, 280 lines, which runs in prebuild, checks every data-shaped number in a post against its sidecar, and exits non-zero on a mismatch:
"prebuild": "npm run check:sources && node scripts/generate-search-index.js && ..."
Putting it in prebuild was deliberate. It blocks npm run build instead of leaving a warning in a log, so the deploy goes red and the site keeps the old version. This is exactly the "something that dares to say no", and its price is 280 lines of JavaScript with no dependencies.
Scheduling is a cron job that runs at 08:00 Vietnam time every Monday:
on:
schedule:
- cron: '0 1 * * 1'
Discovery lives inside that job: it collects every external link in content/, curls each one to see whether it is still alive, then looks at the claims marked volatile whose check date is more than 90 days old.
Handoff is missing. The cron job finds problems and dumps them into Actions' ::warning::, waiting for me to read them. No worktree opens, no agent gets a task, no PR gets created. It finds its own work and blocks wrong work on its own, but it does not do the work, which is why it is only half a loop.
The July 2026 review, run by hand across 10 posts, found 7 real errors, including a sentence that attributed to the Chrome documentation a statement the documentation never made. The sidecar did not stop that sentence from being written. It made the wrong sentence checkable, and that is its entire value.
The next step is clear: have a second job open a worktree for each finding, give an agent the task of drafting the fix, and keep the stopping point at the PR. No auto-merge.
Five ways a loop fails, each one a dropped move
| Failure | Dropped move | Symptom |
|---|---|---|
| Yes-man | Verification | Hundreds of passes without saying "no" once |
| Amnesiac | Persistence | Starts from the same place every morning |
| Manual | Scheduling | The last run was the day of the demo |
| Blind | Discovery | A person still spends the morning deciding what the loop should do |
| Tangled | Handoff | One agent is fine; run five every morning and the merges cannot be untangled |
The yes-man is the most common. A loop that runs hundreds of passes and never once rejects its own work is impossible with any real workload, so it is proof that no check exists there.
These five tend to come together. A loop built in a hurry usually installs exactly the two moves that produce visible results, discovery and handoff, and drops the three moves that produce safety.
Four debts that make no noise
A loop that runs by itself is also a loop that goes wrong by itself. None of the debts below makes a sound while it is running.
Verification debt. Every PR opened and merged saves time, and the time saved turns into output nobody has checked, waiting for the day it comes due. It hides exactly where the tests do not reach, in the gap between "it runs" and "it is right".
Comprehension rot. The codebase grows while the map in your head stands still. Reading code is more boring than writing it, and the loop has already taken the writing.
Cognitive surrender. The more trustworthy the loop gets, the easier it is to stop having opinions, take what it hands you and move on. This is the attitude version of the two debts above: you stop wanting to care, even though you still have the time.
Token blowout. The only debt that hits the bill directly. A bug spinning idle all night leaves you a strange bill in the morning instead of fixed code.
These four feed each other. A loop opens 20 PRs overnight, all tests green, which looks like a big win. Suppose three of the twenty PRs contain subtle bugs the tests never touch: with no independent evaluator, those three get merged; that is verification debt. The person who merged twenty PRs did not read them, so the map in their head falls twenty changes behind; that is comprehension rot. The loop runs so smoothly that the next morning they stop reading altogether; that is cognitive surrender. And because it happily spawned helpers and retries all night, the bill comes in at three times the estimate; that is token blowout.
Three matching safeguards, set before the loop runs, not after the first bill:
- Read a sample every day. Not everything, but force yourself to explain back a few of the changes you read. If you cannot explain them, the map in your head has fallen behind.
- Set hard caps: a budget per run, a budget per day, a maximum number of retries. These numbers are fuses, not money-saving tips.
- Leave one door open, meaning at least one point where the loop stops and waits for a person. You will not need to step in every time; the door keeps you in a position where you still can.
Stripe merges over 1,300 PRs a week with hard gates, not a bigger model
Stripe's Minions system merges over 1,300 pull requests a week, none of them typed by hand, according to engineer Steve Kaliski on the How I AI podcast. The trigger is almost comically light: tag a bot in Slack, or drop an emoji reaction.
The reliability comes from the part before the model wakes up. A deterministic orchestrator gathers the context first: it scans links, pulls Jira, finds docs, and uses Sourcegraph and MCP to locate the relevant code. Letting an LLM go find its own context is the hardest part to control, so that work, which can be written down as rules, is taken out of the model's hands.
The most counterintuitive detail: Minions is not built on a stronger model. It is a fork of Goose, an open-source tool, and its thesis is that reliability comes from the quality of the constraints, not from the size of the model. The architecture interleaves deterministic gates with the LLM's creative steps: the agent writes code, a hard pipeline runs a linter that the agent has no way to skip, the agent fixes the lint, then another hard step runs the commit.
Those 1,300 PRs are still reviewed by people, so people have not left; they have moved from the writing desk to the review desk.
Building your first loop: choose local or cloud first
The only question to answer: is the loop's work tied to your local machine?
/loop on your machine | Cloud schedule | |
|---|---|---|
| Machine must be on | Yes | No |
| Session must be open | Yes | No |
| Shortest interval | minutes | hours |
| Sees local files | Yes | No |
A loop that has to ping the dev server every minute can only run locally, because the cloud cannot see processes on your laptop. A loop that sweeps issues at three in the morning should not be tied to a laptop, because laptops get their lids closed, run out of battery and get carried out of the house.
The common mistake is to treat rerunning in place as "running while I sleep". /loop runs inside the session; the docs describe it as "Run a prompt repeatedly while the session stays open". It means "run a few more passes while I am still sitting here". Only a cloud schedule means "run even when I am not here".
For web developers in Vietnam, this part is cheaper than it sounds. Anyone renting a VPS to host sites already has a machine that never turns off. This repo installed a GitHub Actions self-hosted runner on that very server for the deploy pipeline, and the same runner also runs the weekly source-check cron job. No extra infrastructure, no Actions minutes spent.
Three commands are enough to start, tested on Claude Code 2.1.196:
# run in a loop inside the session, paces itself if you leave out the interval
/loop 5m check if the deploy finished
# run until the condition is met, with a different model judging the condition
/goal all tests in test/auth pass and the lint step is clean
# one worktree per task, so parallel agents do not step on each other
claude --worktree fix/auth-test "draft the fix"
A six-line checklist before you let it run on its own:
- Discovery: what does it read on schedule? CI, issues, commits or an inbox
- State: which file on disk keeps the memory between passes
- Evaluator: is there an independent check that can say "no"
- Isolation: does each parallel agent have its own worktree yet
- Token cap: is there a spending cap, and who stops it when it runs wild
- Human review: which step stops and waits for a person to look
The first two lines decide whether the loop runs at all. The last four decide whether it causes damage once it is running. Beginners usually ship with only the first two.
The real trade-off of handing work to a loop
A loop is a faithful multiplier: bring understanding and it multiplies understanding; bring laziness and it multiplies the laziness just as many times. Two people build the same loop, with ninety percent of the code identical and one or two stopping points different, and six months later one of them stands above the loop while the other has become the gatekeeper for a machine they can no longer read.
Plainly put, this site still falls short in one place: my gate only checks numbers. The three most dangerous kinds of error still have to be caught by reading: a wrong qualitative claim, a correct number attributed to the wrong source, and a real document quoted as saying things it does not say. All three were among the 7 errors in the last review, and the machine caught none of them.
The loop makes generating things almost free. Knowing which of them is right is still scarce, and that is why an engineer still needs to sit here.
Related posts
all posts →- REA: letting an agent reverse engineer software, then testing the oracle tooREA lets a coding agent read a program that has no source, down to machine code, then rebuild that feature in its own code. Whether the rebuild is right is up to the oracle, and the oracle is code too, with its own bugs.read more →
- A Mac with 65% RAM free nearly panicked: the compressor, IOSurface and Playwright's WebKitA 32 GB Mac Studio panicked with 100% of compressed pages limit, then came close to a second panic that evening while RAM still showed 65% free. The page count in the compressor had filled up, and the culprit was IOSurface that Playwright WebKit left in the kernel.read more →
- Top 10 Claude Code skills by GitHub stars: what each one does and which ones overlapThe ten Claude Code skill sets with the most GitHub stars on 29 Oct 2026: Superpowers, Matt Pocock Skills, Karpathy Skills, Anthropic Skills, Ponytail, UI UX Pro Max, Graphify, Caveman, Agent Skills, Taste Skill. Each one with its install command, what it does and where it overlaps with another set.read more →
- Dropping the server: moving a static Next.js site to Cloudflare Workers Static AssetsThis site is only HTML, JS and images, yet it used to live on a 2 GB RAM VPS shared with about 28 other sites. How I moved it to Workers Static Assets: translating the nginx config into _redirects and _headers, why requests do not count against the Free plan limit, and two places I tripped after the move.read more →