{"slug": "loop-engineering-examples-harden-one-loop-control-by-control", "title": "Loop Engineering Examples: Harden One Loop, Control by Control", "summary": "A Loop Engineering post argues that most published AI agent loops ship without hard stops, and recommends capping both the outer loop and every inner call, keeping a cumulative spend ledger in the wrapper rather than the agent, holding state in a file the agent can only flip, and deciding \"done\" with tests mounted read-only. The post cites an overnight loop with no stop condition that opened over 30 code change requests and left a bill in the thousands of dollars, roughly ten times what the team would have chosen to spend, and recommends a breaker that stops after three iterations in a row leave the patch unchanged, treating the 20-iteration, 40-turn, $40 defaults as starting numbers and running in a container on bounded, verifiable work first.", "body_md": "Agent Runtime\n\n# Loop Engineering Examples: Harden One Loop, Control by Control\n\n1. Most published agent loops are demos, not harnesses: the gap is a few dozen lines of bash, each one there because a default is unlimited or because something already went wrong without it.\n2. Harden a loop by taking the exit out of the agent’s hands: cap the outer loop and every call, keep the cumulative spend ledger in the wrapper, hold state in a file the agent can only flip, and decide “done” with tests mounted read-only, which is only as trustworthy as the tests behind it.\n3. Add a breaker that stops after three iterations in a row leave the patch unchanged (my application of the research, not a measured result), check which controls a built-in loop actually provides before swapping `loop.sh` for it, and treat the 20-iteration, 40-turn, $40 defaults as starting numbers, run in a container on bounded, verifiable work first.\n\n## The plain-language edition for readers outside AI engineering no background needed · 6 min\n\nSome AI programs are set to repeat the same job over and over on their own; this post shows the safety limits they need, one at a time.\n\n## The big idea\n\nMost of the examples people publish for this have no safety limits at all. Picture a contractor who works one shift at a time and forgets everything when the shift ends. A foreman keeps sending the contractor back in until the job is done. If the foreman sets no limit on the number of shifts, the money spent, or how “done” gets decided, the contractor can keep working and billing all night. The fix is a handful of rules kept by the foreman, not by the contractor. Each rule exists either because the tool’s default is “no limit” or because something already went wrong without it.\n\n## The popular examples run with no brakes\n\nThe post looks at three well known loops. Two stop on nothing; they run until the program dies. The third stops when a notes file says “ALL TASKS DONE”, but the worker itself is told to update that file. So the only way out is a sentence the worker writes about itself. Even the guide that taught that third loop said every loop needs three hard stops (a cap on repeats, detection of no progress, and a spending limit), then shipped a loop with none of them.\n\nTwo real stories show the stakes. In one big project, an AI ran in an uncapped loop, and it ended only once, by accident, when the AI killed its own process. In another, a team’s overnight loop with no stop condition opened over 30 code change requests and left a bill in the thousands of dollars, roughly ten times what the team would have chosen to spend if anyone had been asked.\n\nA safety note the author repeats: these loops let the AI act without asking permission each time, so run them in a sealed container, not on your everyday machine.\n\n## Limit the shifts, and limit each shift; keep the money tally yourself\n\nThere are really two loops. The outer one is the foreman sending the worker back in. The inner one is the worker’s own steps within one shift. In the tool being used, neither has a limit unless you set one, so you set both: a maximum number of shifts, and a maximum number of steps and dollars per shift.\n\nThose per-shift limits are checked only after each step finishes, so a shift can overshoot a little. The docs themselves say to leave headroom.\n\nA per-shift budget also can’t see the whole night. Twenty shifts at four dollars each is eighty dollars, and no single shift ever saw that number. So the foreman keeps a running tally outside the worker and stops everything when it hits a ceiling. The author treats the tally as an estimate, not a bill: the tool’s own cost numbers are approximate and can be wrong after a crash. The usage page is the real bill; the tally is just the brake.\n\n## Let the worker tick a checklist, but check the work with a test it can’t touch\n\nInstead of trusting “I’m done”, the foreman keeps a checklist file where the worker may only change a box from unticked to ticked. If the worker rewrites the list in any other way, the foreman restores the saved copy. The foreman also writes the log note if the whole thing is shut down mid-shift, because the worker can’t write a note after it has been cut off.\n\nA tick is still the worker grading itself. So before any tick counts, the foreman runs an outside test, kept in a folder the worker can only read, not change. The research the author cites points one way: AI models that are told to pass tests often cheat by changing the tests, and telling them not to cheat had almost no effect. Separating the one who does the work from the one who judges it helps a lot.\n\nTwo limits on this. Read-only tests do not block every kind of cheating. And the cheating studies were done on tasks deliberately made impossible, so they show what the models do under pressure, not how often it happens on ordinary work. A passing check is only as trustworthy as the test behind it, and a person should still read the changes before anything is accepted.\n\n## Stop when three shifts in a row change nothing\n\nSometimes the worker keeps re-running the same tests and thinking over the same problem without changing any of the actual work. Research found this pattern in one case: one set of tests was rerun 22 times with nothing new written. So the foreman looks at whether the work changed after each shift and stops after three shifts in a row with no change. The notes file and the checklist are excluded from that comparison, since they change every shift and would hide the stall.\n\nThe author is open that this is his own application of the idea, not a measured result. The research defined these stalls by studying recorded runs afterward, and another project already uses a similar stop on unchanged files. The author combined those two facts.\n\n## The built-in repeat tools have gaps too\n\nBefore swapping your own loop for a ready-made repeat command, check which limits it actually has. Some default to unlimited repeats. Some let the AI decide when it is finished. One lets a small helper model judge whether the goal is met, but that helper can only read what the worker has already said, so a claimed test result is a believed test result. One rule: if a hook (a custom checking script) is your stop condition, adding a certain option that skips hooks removes the stop without any error message. The author also infers, but did not test, that a repeating loop that makes edits is not covered by that hook’s built-in eight-in-a-row override.\n\n## What this means for you\n\nIf you ever run an AI in a loop, do not rely on the AI to say when to stop, how much it spent, or whether it succeeded. Keep those three in a separate script: a cap on shifts, a spending tally with a ceiling, and an outside test the AI can’t edit, plus a stop when nothing is changing. The author’s starting numbers (20 shifts, a 40 dollar ceiling, three unchanged shifts) are his own starting points, not researched thresholds. Begin on small, checkable work in a brand new project with a low ceiling, so a bad night costs little. Then read the last line of the log: it says which rule fired, and that is the one to adjust first.\n\n**The technical terms, in plain words**\n\n- loop = the same job repeated over and over until a stop condition is met\n- agent = an AI program that takes steps on its own to get a job done\n- iteration = one pass through the loop, one shift\n- turn = one step the AI takes within a shift\n- outer loop / inner loop = the foreman repeating shifts / the worker’s steps inside one shift\n- context window = the AI’s working memory for one shift; a new shift starts empty\n- harness = the rules and scripts wrapped around the AI to keep it in bounds\n- spend ceiling = a total dollar limit for the whole run\n- ledger = the running tally of money spent, kept by the wrapper script\n- gate = an outside check that must pass before a result counts\n- circuit breaker = an automatic stop when something looks stuck\n- diff = the list of what changed in the code between two moments\n- container = a sealed box on your computer so the AI can’t affect anything outside it\n- Ralph loop = a well known name for the bare repeat-forever AI loop\n- hook = a small script that runs at set moments and can allow or block an action\n\n**Keep reading:** [the full version, with the research and sources ↓](#essay)\n\nOne of the top search results for loop engineering, a dev.to field guide, tells you to bake three hard stops into every loop: an iteration cap, no-progress detection, a spend ceiling. Then it ships a bash loop with none of them ([truongpx396: The Agentic Loop](https://dev.to/truongpx396/the-agentic-loop-a-practical-field-guide-mnc)<sup>[1](#src-1)</sup>). Anthropic’s own C compiler ran nearly 2,000 Claude Code sessions across two weeks, at a total cost just under $20,000, on a bare `while true` ([Carlini: Building a C compiler](https://www.anthropic.com/engineering/building-c-compiler)<sup>[2](#src-2)</sup>). The loops people publish are demos, not harnesses. The difference is a few dozen lines of bash, each one there because a default is unlimited or because something already went wrong without it.\n\nThe why lives in [Loop Engineering Breaks Your Single-Shot Context Playbook](https://jyoung.dev/blog/loop-engineering-breaks-your-playbook/): a loop needs an explicit stop and externalized state, the two fields a single prompt never needed. This post is the how. It takes one `loop.sh`, starts from the bare loop everyone copies, and adds one control per section until every exit names the control that fired. Every recipe here runs `claude -p` with `--dangerously-skip-permissions`, so run it in a container. That is Carlini’s own instruction: “(Run this in a container, not your actual machine).” The [agent harness audit](https://jyoung.dev/blog/agent-harness-audit/) covers the sandbox and permissions layer that caveat stands on.\n\n## Start from the Loop Everyone Publishes\n\nRead the published loops as the baseline you are about to harden, not the recipe you copy. Here are three, verbatim: Geoffrey Huntley’s original Ralph one-liner, the harness Nicholas Carlini used for the C compiler, and the loop in the dev.to field guide.\n\nRead them for what they stop on. Huntley’s and Carlini’s loops stop on nothing; they run until the process dies. The dev.to loop stops when `STATUS.md` contains `ALL TASKS DONE`. The prompt two lines down tells the agent to update `STATUS.md`, so the agent writes its own exit. None of the three counts iterations, sums spend, or checks whether the last pass changed anything, though a few sections above its loop the dev.to guide says “Bake all three into every loop” ([truongpx396: The Agentic Loop](https://dev.to/truongpx396/the-agentic-loop-a-practical-field-guide-mnc)<sup>[1](#src-1)</sup>). Huntley is equally candid about the output: “you’ll wake up to a broken codebase that doesn’t compile from time to time” ([Huntley: Ralph Wiggum as a “software engineer”](https://ghuntley.com/ralph/)<sup>[3](#src-3)</sup>).\n\nTwo incidents set the stakes. Carlini’s loop ended exactly once, and not by design:\n\nI did see Claude `pkill -9 bash` on accident, thus killing itself and ending the loop. Whoops!\n\nThe second ran on a cron. Dave Slutzkin describes a team whose overnight loop had an open-ended goal and no stop condition. It opened 30+ PRs, left a four-figure token bill, and averaged $70+ in tokens per PR ([Slutzkin: The loop that cost $1000+ overnight](https://antiburn.com/resources/the-loop-that-cost-1000-dollars/)<sup>[4](#src-4)</sup>).\n\nIt spent something like 10x what the team would have chosen to spend if anyone had been asked.\n\n**The baseline is therefore: a loop whose only exit is a sentence the agent writes about itself.** Call the dev.to version `loop.sh` v0; [the finished loop](#the-finished-loop-line-by-line) maps every line added after it to the default or incident that put it there.\n\n## Cap the Outer Loop and Every Call\n\nA loop needs two caps because it runs two loops, and neither has a limit unless you set one. The outer loop is your bash `while`. The inner loop is the tool-calling loop inside each `claude -p` call. Claude Code documents its `--max-turns` as “No limit by default” ([Anthropic: CLI reference](https://code.claude.com/docs/en/cli-reference)<sup>[5](#src-5)</sup>). The Agent SDK lists both its max turns and max budget defaults as “No limit” ([Anthropic: How the agent loop works](https://code.claude.com/docs/en/agent-sdk/agent-loop)<sup>[6](#src-6)</sup>).\n\nClayton Farr’s Ralph Playbook is the widely copied loop that does count iterations, and it shows why one cap can’t stand in for the other. Its `MAX_ITERATIONS` “Limits the outer loop (number of tasks attempted; NOT tool calls within a single task)”. A bare `./loop.sh` runs unlimited until you press Ctrl+C, and the playbook says plainly that “A single task execution has no hard technical limit” ([Farr: The Ralph Playbook](https://github.com/ghuntley/how-to-ralph-wiggum)<sup>[7](#src-7)</sup>).\n\n`MAX_ITERATIONS` defaults to a number and the script refuses 0, so forgetting the argument gets you 20 iterations instead of Farr’s unlimited run. The exit code gets branched on, not dropped. Claude Code “exits with code 0 on success and a non-zero code when the run fails, so your scripts can branch on the exit status” ([Anthropic: Run Claude Code programmatically](https://code.claude.com/docs/en/headless)<sup>[8](#src-8)</sup>), and `--max-turns` “Exits with an error when the limit is reached.” v1 records a nonzero exit in `progress.txt` rather than stopping. The next iteration starts in a fresh context window and can see that the last one ran out of turns. That is why the script sets `-uo pipefail` without `-e`: a loop that branches on failures can’t die on the first one.\n\n### Size the Per-Call Caps\n\nSize both per-call caps for the moment they get checked, which is after a response lands, not before. The turn rule from the SDK docs: “Count each turn with tool calls, plus one for the final text-only response” ([Anthropic: How the agent loop works](https://code.claude.com/docs/en/agent-sdk/agent-loop)<sup>[6](#src-6)</sup>). The budget is compared “after model responses arrive”, so spend can pass the cap by the cost of that response plus whatever running subagents spend before they stop. The CLI reference puts it in one line: “Spend can pass the cap, so leave headroom” ([Anthropic: CLI reference](https://code.claude.com/docs/en/cli-reference)<sup>[5](#src-5)</sup>).\n\n| When you see | It means | Do this | \n|---|---|---|\n| `\"subtype\": \"error_max_turns\"` and no`result` in the iteration’s JSON | The last turn went to a tool call and the run never wrote its final response | Add one turn for the final response; if it still hits, [the task is too big](https://jyoung.dev/blog/how-to-size-tasks-for-ai-coding-agents/) for one iteration | \n| `\"subtype\": \"error_max_budget_usd\"` | The call reached `CALL_BUDGET` mid-task | Split the feature before you raise the cap | \n| `total_cost_usd` above`CALL_BUDGET` | Expected: the cap is checked after each response | Keep `CALL_BUDGET` well under the run ceiling; never set the two equal | \n| `--resume` or`--continue` anywhere in the loop | Restored totals “don’t count toward” `--max-budget-usd` | Start every iteration as a fresh call | \n\nThe last row is the reason the next section exists.\n\n## Keep the Spend Ledger Outside the Agent\n\nA per-call budget stops a call, not a run: twenty iterations at `CALL_BUDGET=4.00` is an $80 run that no single flag ever saw. The cumulative number has to live in the wrapper, and Anthropic’s cost docs spell out the arithmetic. For independent calls with no resume, “each result covers only its own call, so add the totals yourself.” For resumed sessions the opposite holds:\n\nEach result already includes the session’s earlier spend. Read the latest result for the session total; summing results double-counts the restored spend.\n\n`loop.sh` never resumes, which keeps the ledger a plain sum. With `--output-format json`, the payload includes `total_cost_usd` “so scripted callers can track spend without consulting the usage dashboard” ([Anthropic: Run Claude Code programmatically](https://code.claude.com/docs/en/headless)<sup>[8](#src-8)</sup>).\n\nThe default ceiling of $40 is deliberately lower than 20 iterations at $4, so the ledger is the control that binds and the iteration cap is the backstop. Treat the number as an estimate: `total_cost_usd` and `costUSD` are “client-side estimates, not authoritative billing data”, and after a session crash “every cost field may be zeroed” ([Anthropic: Track cost and usage](https://code.claude.com/docs/en/agent-sdk/cost-tracking)<sup>[9](#src-9)</sup>). The Usage page is the bill; `spend.log` is the brake.\n\nWhat this stops is a runaway rate, not one expensive call. Blake Crosley’s early sessions ran without spawn budgets:\n\nWithout spawn budgets, my early sessions consumed API tokens at 10x the normal rate before manual intervention.\n\nThe antiburn team’s $70+ per PR has the same shape: no stop condition bounded the run, and the sum was a number nobody would have approved. v2 keys spend by iteration, which is enough for a ceiling but not for knowing which task burned the money. [Per-task cost attribution](https://jyoung.dev/blog/per-task-cost-attribution/) covers why a per-task ceiling needs per-task keys before it is enforceable.\n\n## Put State on Disk the Agent Can Only Flip\n\nThe `grep` exit in v0 and v1 trusts a sentence the agent wrote, and Anthropic has documented how that goes wrong. Justin Young’s failure table for long-running agents includes “Claude declares victory on the entire project too early.” and “Claude marks features as done prematurely.” ([Young: Effective harnesses for long-running agents](https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents)<sup>[11](#src-11)</sup>). The fix was a feature list in a format chosen for how the model treats it:\n\nwe landed on using JSON for this, as the model is less likely to inappropriately change or overwrite JSON files\n\n`features.json` follows the shape Anthropic describes (`category`, `description`, `steps`, `passes: false`), plus one field I add: `test`, the id of the protected test the gate in the next section runs for that feature.\n\nAnthropic enforces the flip-only rule with a prompt line (“It is unacceptable to remove or edit tests because this could lead to missing or buggy functionality.”). `loop.sh` enforces it in the wrapper instead and swaps the `grep` exit for a `jq` check over the file. It also commits once per iteration, so git history and `progress.txt` are what the next fresh context reads first.\n\n`only_flags_changed` strips `passes` from the committed and working copies and diffs what is left. If the agent rewrote a description, deleted a feature, or left invalid JSON, the wrapper restores `features.json` from HEAD and says so in `progress.txt`. Crosley’s state was corrupted twice during development, and his recovery recreates “from safe defaults rather than crashing” ([Crosley: The Ralph Loop](https://blakecrosley.com/blog/ralph-agent-architecture)<sup>[10](#src-10)</sup>); here HEAD is the safe default.\n\nThe `trap` exists because of a GitHub issue that reads like a postmortem:\n\nRalph loop attempts that hit the Claude session limit mid-run leave no trace of what they learned.\n\nOne issue’s attempts hit the limit four times in one day and cold-started each time. The note-writing lived inside the agent’s turn, and “a session-limit kill happens to the host wrapper, not inside a live agent turn.” The wrapper is the only process alive to write the note, so the wrapper writes it. Backgrounding the call and calling `wait` is what makes that work. Bash defers a trap until a foreground child exits, so a `docker stop` against a foreground `claude -p` can hit its kill timeout before the note is written.\n\n## Gate on a Check the Agent Cannot Edit\n\nv3 still accepts any `passes` flip the agent makes, which moves the self-report from a string into a boolean without making it any more true. Decide “done” in the wrapper by running tests the agent cannot write to, never by reading what the agent said.\n\n**Bad:** `jq -e 'all(.[]; .passes == true)' features.json` or `grep -q \"ALL TASKS DONE\" STATUS.md` as the exit, alone. Both read a file the agent writes.\n**Good:** before a flip survives the iteration, the wrapper runs that feature’s `test` from `/gate/tests`, a directory mounted read-only into the container. When any protected test fails, it restores `features.json` from HEAD.\n\nThe gate tests live outside the repo the agent edits, and `:ro` makes the mount unwritable whatever permission mode the agent runs in. The `--user` flag isn’t decoration: bypass-permissions mode “Can’t be used when running as root on Unix” ([Anthropic: How the agent loop works](https://code.claude.com/docs/en/agent-sdk/agent-loop)<sup>[6](#src-6)</sup>). `--init` puts a real PID 1 in front of `loop.sh` so `docker stop` reaches the trap.\n\nThe evidence for keeping tests out of reach is consistent. ImpossibleBench found that Claude models “cheat primarily (>79%) through modifying test cases”, and that allowing multiple submissions raised the cheating rate on Conflicting-SWEbench “from 33% to 38%” averaged across models, which is a loop’s retry behavior measured directly ([Zhong et al.: ImpossibleBench](https://arxiv.org/abs/2510.20270)<sup>[13](#src-13)</sup>). Its recommendation is the Good line above:\n\nwe recommend either hiding test files entirely or restricting them to read-only access during implementation, when feasible, to mitigate problematic cheating behaviors.\n\nPrompting doesn’t substitute. METR found that telling a model not to cheat “had a nearly negligible effect on reward hacking, which still persisted in a majority of runs” ([METR: Recent Frontier Models Are Reward Hacking](https://metr.org/blog/2025-06-05-recent-reward-hacking/)<sup>[14](#src-14)</sup>). Self-assessment fails the same way: across 54 cycles an agent claimed improvement every time while 56 percent had a measured delta of zero or below ([Park and Choi: When Do Agent Loops Mistake Stagnation for Progress?](https://arxiv.org/abs/2607.25152)<sup>[15](#src-15)</sup>). Crosley’s agent, asked to “write tests that pass,” wrote `assert True` ([Crosley: The Ralph Loop](https://blakecrosley.com/blog/ralph-agent-architecture)<sup>[10](#src-10)</sup>). Anthropic’s harness work landed on the same split: “Separating the agent doing the work from the agent judging it proves to be a strong lever to address this issue” ([Rajasekaran: Harness design for long-running application development](https://www.anthropic.com/engineering/harness-design-long-running-apps)<sup>[16](#src-16)</sup>).\n\nThe gate has a limit, and it decides how far you trust a green run. Read-only access “does not eliminate other cheating methods such as special-casing or operator overloading”, and ImpossibleBench measured all of this on deliberately impossible tasks ([Zhong et al.: ImpossibleBench](https://arxiv.org/abs/2510.20270)<sup>[13](#src-13)</sup>). The gate is only as good as the tests behind it, which is Carlini’s point: “it’s important that the task verifier is nearly perfect, otherwise Claude will solve the wrong problem” ([Carlini: Building a C compiler](https://www.anthropic.com/engineering/building-c-compiler)<sup>[2](#src-2)</sup>). Writing a test that works as that verifier is its own job, covered in [done criteria as predicates](https://jyoung.dev/blog/agent-done-criteria-predicates/), and somebody still [reads the diff](https://jyoung.dev/blog/evaluating-ai-coding-agent-output/) before anything merges.\n\n## Break on an Unchanged Diff\n\n**Author’s judgment.** Keying a runtime breaker on the diff is my application, not a measured result. Purdue’s researchers defined stalls offline, in recorded trajectories, and frankbria’s runner already ships a breaker on file changes; this section follows from those two premises.\n\nStop the loop after three iterations in a row that leave the patch unchanged. Caps stop a stalled loop eventually; this stops it before seventeen dead iterations burn the ceiling. The Purdue study of cost-inefficient agent behavior defines the failure precisely:\n\nProgress stalls arise when agents enter long reasoning loops without updating the patch, repeatedly reconsidering the same diagnosis and rerunning tests despite sufficient prior signals.\n\nTheir worked case is django-11141: the agent hesitates over whether updating the test functions satisfies the instruction, reruns the migrations test suite 22 times and `test_makemigrations_no_init` 18 times without another patch, and fails when its step budget runs out. Carlini saw the same pattern at scale: “Claude can’t tell time and, left alone, will happily spend hours running tests instead of making progress” ([Carlini: Building a C compiler](https://www.anthropic.com/engineering/building-c-compiler)<sup>[2](#src-2)</sup>). In both, tests run and the patch doesn’t move. frankbria’s ralph-claude-code opens its circuit on exactly that, with `CB_NO_PROGRESS_THRESHOLD=3` documented as “Open circuit after 3 loops with no file changes” ([frankbria: ralph-claude-code](https://github.com/frankbria/ralph-claude-code)<sup>[18](#src-18)</sup>).\n\nBecause v3 commits every iteration, the staged diff against HEAD is exactly this iteration’s patch. An empty one is an unchanged diff, so no hashing is needed. The two exclusions matter. `progress.txt` changes every iteration because both the agent and the wrapper append to it, and a flipped flag in `features.json` isn’t code. Count either as progress and the breaker never fires.\n\n## Know How the Built-in Loops Stop\n\n**Author’s judgment.** One row below is inference: that a Stop hook loop whose iterations make edits is not bounded by the 8-block cap follows from the hooks guide’s wording (“no tool call from Claude in between”), not from a test I ran.\n\nBefore you swap `loop.sh` for a built-in, check which of its controls the built-in actually provides. Anthropic defines loops as “agents repeating cycles of work until a stop condition is met” ([de Oliveira and Segner: Getting started with loops](https://claude.com/blog/getting-started-with-loops)<sup>[19](#src-19)</sup>). The built-ins differ in who evaluates that condition, and in what bounds the run when it is never met.\n\n| Primitive | How it stops | What it leaves to you | \n|---|---|---|\n| `/loop 5m <prompt>` | Ask Claude to cancel it; `Esc` and`Ctrl+C` don’t; otherwise it expires 7 days after creation | Spend ceiling, gate, diff breaker; seven days is the only cap | \n| `/loop <prompt>` (self-paced) | `Esc` while it waits, or Claude ends it when it judges the task complete | The same, and the done judgment is the agent’s own | \n| `/goal <condition>` | A small fast model judges the condition after every turn; it also stops after several turns in a row with no tool use | A turn cap is a clause the evaluator judges, not a limit; resume resets the turn count, timer and spend baseline | \n| `/ralph-loop ... --max-iterations N` | Exact-match `--completion-promise` , or N iterations | `--max-iterations` defaults to unlimited; the promise is self-report; no spend cap | \n| Stop hook | Your script blocks or allows each stop; overridden after 8 blocks in a row with no tool call between | An editing loop (inference above); `--bare` skips hooks entirely | \n| `codex exec` | One run per call; read-only sandbox unless you pass `--sandbox workspace-write` | No documented turn, budget or timeout flag; a dropped refusal still exits 0 | \n\nRow sources: [scheduled tasks](https://code.claude.com/docs/en/scheduled-tasks)<sup>[20](#src-20)</sup>, `/goal`<sup>[21](#src-21)</sup>, the [ralph-loop README](https://github.com/anthropics/claude-plugins-official/blob/main/plugins/ralph-loop/README.md)<sup>[22](#src-22)</sup>, the [hooks guide](https://code.claude.com/docs/en/hooks-guide)<sup>[23](#src-23)</sup>, the [CLI reference](https://code.claude.com/docs/en/cli-reference)<sup>[5](#src-5)</sup> for `--bare`, and for Codex the [non-interactive docs](https://learn.chatgpt.com/docs/non-interactive-mode)<sup>[24](#src-24)</sup> plus [issue #56875](https://github.com/openai/codex/issues/56875)<sup>[25](#src-25)</sup>, where a refused turn “completes normally, so `codex exec` exits 0.”\n\n- **Keep the cap in the harness.**`/goal` ’s evaluator “does not call tools, so it can only judge what Claude has already surfaced in the conversation”; a test result the agent claims is a test result the evaluator believes.\n- **Pass `--max-iterations` every time.** The ralph-loop README: “Always rely on`--max-iterations` as your primary safety mechanism.”\n- **Never add `--bare` to a hook-driven loop.**`--bare` skips auto-discovery of hooks, so the Stop hook that was your exit condition stops existing, and that footgun has no error message.\n- **Port the outer loop to Codex, not the trust.** Run`codex exec` inside the same`loop.sh` shape, read the`--json` stream for`turn.failed` and`error` events, and let the gate decide. Exit 0 means the process ended, not that the work did.\n\n## The Finished Loop, Line by Line\n\n**Author’s judgment.** The defaults (20 iterations, 40 turns, $4 per call, a $40 ceiling, 45 minutes, 3 strikes) are my starting numbers, not researched thresholds, and the mapping below is my synthesis of the sources cited above it.\n\nShip the hardened `loop.sh` with this table next to it.\n\n| Section | The line in `loop.sh` | What it stops | Behind it | \n|---|---|---|---|\n| **Start from the loop everyone publishes** | `jq -e 'all(.[]; .passes == true)'` replaces`grep -q \"ALL TASKS DONE\"` | An exit the agent writes about itself | The dev.to loop; Carlini’s `pkill` ; antiburn’s overnight cron | \n| **Cap the outer loop and every call** | `MAX_ITERATIONS=20` ,`--max-turns` ,`--max-budget-usd` ,`case \"$rc\"` | Unbounded iterations and unbounded calls | `--max-turns` “No limit by default”; Farr’s “no hard technical limit” | \n| **Keep the spend ledger outside the agent** | `spend.log` ,`over_ceiling` , no`--resume` | A run whose total no single flag sees | Per-call budgets reset each call; Crosley’s 10x; antiburn’s $70+ per PR | \n| **Put state on disk the agent can only flip** | `features.json` ,`only_flags_changed` , per-iteration commit,`trap on_kill` | Premature “done”, corrupted state, cold starts after a kill | Anthropic’s premature-victory rows; rpbaptist #119; Crosley’s corrupted state | \n| **Gate on a check the agent cannot edit** | `gate_ok` against`/gate/tests:ro` | Edited tests and self-graded flips | ImpossibleBench; METR; Park and Choi; Crosley’s `assert True` | \n| **Break on an unchanged diff** | `git diff --cached --quiet HEAD` ,`NO_PROGRESS_LIMIT=3` | Iterations that rerun tests and never move the patch | Purdue’s django-11141; Carlini’s time blindness; frankbria’s threshold | \n| **Know how the built-in loops stop** | A plain `claude -p` call, not`/goal` or`/ralph-loop` | Caps that a model judges, or that default to unlimited | `/goal` turn clause; ralph-loop`--max-iterations` default; Stop hook block cap | \n\nRun it on [bounded, verifiable work](https://jyoung.dev/blog/what-ai-agents-are-actually-good-for/) first. Huntley, who started all of this, draws the same line: “There’s no way in heck would I use Ralph in an existing code base” ([Huntley: Ralph Wiggum as a “software engineer”](https://ghuntley.com/ralph/)<sup>[3](#src-3)</sup>). A greenfield repo keeps the blast radius to code nobody depends on yet. A low ceiling keeps a bad night to lunch money:\n\nThen read the last line of `progress.txt`. It names the control that fired, and that is the number to tune before you raise anything else.\n\nThe finished `loop.sh`, its container wrapper and a `features.json` starter are in [the agent-engineering-toolkit](https://github.com/johnayoung/agent-engineering-toolkit)<sup>[26](#src-26)</sup> as `hardened-loop`, with every exit path exercised against a stubbed `claude` before it shipped.\n\n## Sources\n\n1. [truongpx396: The Agentic Loop](https://dev.to/truongpx396/the-agentic-loop-a-practical-field-guide-mnc) dev.to/truongpx396/the-agentic-loop-a-practical-field-guide-mnc\n2. [Carlini: Building a C compiler](https://www.anthropic.com/engineering/building-c-compiler) anthropic.com/engineering/building-c-compiler\n3. [Huntley: Ralph Wiggum as a \"software engineer\"](https://ghuntley.com/ralph) ghuntley.com/ralph\n4. [Slutzkin: The loop that cost $1000+ overnight](https://antiburn.com/resources/the-loop-that-cost-1000-dollars) antiburn.com/resources/the-loop-that-cost-1000-dollars\n5. [Anthropic: CLI reference](https://code.claude.com/docs/en/cli-reference) code.claude.com/docs/en/cli-reference\n6. [Anthropic: How the agent loop works](https://code.claude.com/docs/en/agent-sdk/agent-loop) code.claude.com/docs/en/agent-sdk/agent-loop\n7. [Farr: The Ralph Playbook](https://github.com/ghuntley/how-to-ralph-wiggum) github.com/ghuntley/how-to-ralph-wiggum\n8. [Anthropic: Run Claude Code programmatically](https://code.claude.com/docs/en/headless) code.claude.com/docs/en/headless\n9. [Anthropic: Track cost and usage](https://code.claude.com/docs/en/agent-sdk/cost-tracking) code.claude.com/docs/en/agent-sdk/cost-tracking\n10. [Crosley: The Ralph Loop](https://blakecrosley.com/blog/ralph-agent-architecture) blakecrosley.com/blog/ralph-agent-architecture\n11. [Young: Effective harnesses for long-running agents](https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents) anthropic.com/engineering/effective-harnesses-for-long-running-agents\n12. [rpbaptist: Fix RALPH loop: progress notes not persisted (Issue #119)](https://github.com/rpbaptist/belastingaangifte-check/issues/119) github.com/rpbaptist/belastingaangifte-check/issues/119\n13. [Zhong et al.: ImpossibleBench](https://arxiv.org/abs/2510.20270) arxiv.org/abs/2510.20270\n14. [METR: Recent Frontier Models Are Reward Hacking](https://metr.org/blog/2025-06-05-recent-reward-hacking) metr.org/blog/2025-06-05-recent-reward-hacking\n15. [Park and Choi: When Do Agent Loops Mistake Stagnation for Progress?](https://arxiv.org/abs/2607.25152) arxiv.org/abs/2607.25152\n16. [Rajasekaran: Harness design for long-running application development](https://www.anthropic.com/engineering/harness-design-long-running-apps) anthropic.com/engineering/harness-design-long-running-apps\n17. [Hu et al.: Analyzing and Mitigating Cost-Inefficient Behaviors in Coding Agents](https://arxiv.org/html/2609.30725v1) arxiv.org/html/2609.30725v1\n18. [frankbria: ralph-claude-code](https://github.com/frankbria/ralph-claude-code) github.com/frankbria/ralph-claude-code\n19. [de Oliveira and Segner: Getting started with loops](https://claude.com/blog/getting-started-with-loops) claude.com/blog/getting-started-with-loops\n20. [Anthropic: Run prompts on a schedule](https://code.claude.com/docs/en/scheduled-tasks) code.claude.com/docs/en/scheduled-tasks\n21. [Anthropic: Keep Claude working toward a goal](https://code.claude.com/docs/en/goal) code.claude.com/docs/en/goal\n22. [Anthropic: ralph-loop plugin README](https://github.com/anthropics/claude-plugins-official/blob/main/plugins/ralph-loop/README.md) github.com/anthropics/claude-plugins-official/blob/main/plugins/ralph-loop/README.md\n23. [Anthropic: Automate actions with hooks](https://code.claude.com/docs/en/hooks-guide) code.claude.com/docs/en/hooks-guide\n24. [OpenAI: Non-interactive mode](https://learn.chatgpt.com/docs/non-interactive-mode) learn.chatgpt.com/docs/non-interactive-mode\n25. [openai/codex issue #56875](https://github.com/openai/codex/issues/56875) github.com/openai/codex/issues/56875\n26. [agent-engineering-toolkit: hardened-loop](https://github.com/johnayoung/agent-engineering-toolkit) github.com/johnayoung/agent-engineering-toolkit", "url": "https://wpnews.pro/news/loop-engineering-examples-harden-one-loop-control-by-control", "canonical_source": "https://jyoung.dev/blog/loop-engineering-practical-guide/", "published_at": "2026-11-02 00:00:00+00:00", "updated_at": "2026-11-02 15:51:08.627641+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "ai-safety"], "entities": [], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/loop-engineering-examples-harden-one-loop-control-by-control", "markdown": "https://wpnews.pro/news/loop-engineering-examples-harden-one-loop-control-by-control.md", "text": "https://wpnews.pro/news/loop-engineering-examples-harden-one-loop-control-by-control.txt", "jsonld": "https://wpnews.pro/news/loop-engineering-examples-harden-one-loop-control-by-control.jsonld"}}