
Loop Engineering — Building Autonomous Loops with Claude Code
This page has been translated by machine translation. View original
Introduction
AI Agents (coding agents) like Claude Code and Codex —
the techniques for running them efficiently in actual development have evolved as follows since their release.
- Prompt engineering: How to write a single instruction
- Context engineering: How to organize the information (context) passed to the model each time
- Harness engineering: How to build the environment (tools, permissions, verification) where agents work efficiently
Every few months a new "◯◯ engineering" term appears,
and the latest one is Loop Engineering.
One site summarizes it as: "In less than 18 months, developers moved from prompt engineering → context engineering → harness engineering → loop engineering."
Also, here's a quote from Anthropic's Boris Cherny (the person who created the prototype of Claude Code):
I don't prompt Claude anymore. I have loops running that prompt Claude and figuring out what to do. My job is to write loops.
In this article, we'll review the overview of loop engineering,
then actually build an autonomous loop using Claude Code's Stop hook in a minimal configuration.
We'll also compare that loop across multiple implementation patterns: bash, /goal, skills, subagents, Agent SDK, and more.
About Loop Engineering
The term's emergence
The person who systematized the term "Loop Engineering" is Google engineer Addy Osmani.
In his blog post Loop Engineering on June 7, 2026, he defines it as:
Loop engineering is replacing yourself as the person who prompts the agent. You design the system that does it instead.
The background includes the previously mentioned Boris Cherny quote and
OpenClaw author Peter Steinberger's post on X:
Here's your monthly reminder that you shouldn't be prompting coding agents anymore. You should be designing loops that prompt your agents.
Osmani describes loop engineering as "one floor above the harness."
If the harness is "the environment where a single agent works,"
then the loop is the control structure that "starts that environment on a timer, prepares helpers, and assigns itself the next job."
The relationship between each layer is as follows:
| Layer | Optimization target | Typical question |
|---|---|---|
| Prompt engineering | A single instruction | How to write it to get the intended answer |
| Context engineering | Context per turn | What to put in context and when |
| Harness engineering | Execution environment | Is the environment one where the agent can perform and verify work? |
| Loop engineering | Iterative control structure | How to find the next job, how to verify it, and when to stop |
Note that the lower layers don't become unnecessary.
Prompts, context, and harness all remain —
loop engineering adds autonomous iteration on top of them.
What is a closed loop?
First, let's look at the difference between the old approach and loop engineering using a concrete example in Claude Code.
A common scenario: "tests are failing and I want to fix them."
↓ The old way (humans run the loop):
❯ Fix the failing tests
(check results)
❯ fizzbuzz(15) is still failing. Fix it
(check results)
❯ Fixed. Now make lint pass too
(check results)…
The human handles observation, verification, and issuing the next instruction (loop control) each time.
This means even if Claude is fast, the human's working time doesn't decrease.
↓ Loop engineering:
❯ /goal ./test.sh exits with exit 0. Try 5 turns, stop if it fails
As I'll explain later, /goal above is a Claude Code built-in command that
automatically sends Claude back to work until the specified condition is met.
In this way, verification and continuation decisions are moved to a mechanism (hooks, scripts, etc.),
and humans take on the role of designing stopping conditions and checking results at the end.
This — "replacing the repetitive control that humans handled with a mechanism" —
is loop engineering.
Incidentally, Anthropic describes agents simply as follows:
Agents can handle sophisticated tasks, but their implementation is often straightforward. They are typically just LLMs using tools based on environmental feedback in a loop.
The "loop" that loop engineering deals with is a structure that repeats the following cycle until a stopping condition is met:
Plan Decide what to do
↓
Action Write code / execute commands
↓
Observe Read execution results like test results and errors
↓
Verify Evaluate against the goal
├─ OK … Stopping condition met → end loop
└─ NG … Feed back results and return to Plan (continue loop)
Unlike getting a response from a single prompt (one-shot),
in a closed loop the agent observes feedback from the real environment
(test results, command output, etc.),
evaluates against the goal, and then decides the next action.
Rather than continuing work in the wrong direction indefinitely,
it catches its own mistakes along the way and autonomously corrects course.
The key points that determine loop success or failure are:
- Make verification (Verify) real
Judge "done" based on mechanically determinable evidence like test results and execution output,
not the AI's self-report.
The official Claude blog also states that "the more quantitative the check,
the easier it is for Claude to self-verify."
- Clearly define the stopping condition (termination)
Define completion in a mechanically verifiable form, like "test.sh exits with exit 0" rather than "fix the bug."
Tasks where this can't be written are simply not suited for autonomous loops.
A technique to improve verification quality is separating maker and checker.
If you have a single model "create and then self-score,"
it tends to miss its own bugs (self-scoring bias).
Anthropic introduces this as the evaluator-optimizer pattern here.
(One LLM generates, another LLM evaluates and feeds back in a loop)
4 loop types classified by the official Claude Code blog
The Anthropic Claude Code team, in "Getting started with loops,"
defines a loop as "an agent repeating work cycles until a stopping condition is met"
and classifies them into the following 4 types:
| Loop | Trigger | Stopping condition | Main tools |
|---|---|---|---|
| Turn-based | User's prompt | Claude judges "complete" or "needs additional context" | verification skill |
| Goal-based | Manual prompt | Goal achieved or max turns reached | /goal |
| Time-based | Specified time interval (e.g., every 5 minutes) | Cancel or work complete | /loop, /schedule |
| Proactive | Event/schedule (humans are async) | Each task ends on goal achievement. The routine itself continues until stopped | Combination of all the above |
※ The table is organized based on descriptions from the official blog
Verification skill is a mechanism where you write "the verification steps to always perform before reporting completion" in SKILL.md,
letting Claude self-verify.
The official blog example is a procedure that says "don't report a UI change as complete just because the edit succeeded —
actually verify it works on the dev server and that tests pass before reporting,"
serving as a turn-based loop enhanced with verification.
/goal is a command that defines completion conditions.
Each time Claude tries to stop, an evaluator model checks the condition,
and sends Claude back to work until it's achieved or the specified turn count is reached.
Example:
/goal get the homepage Lighthouse score to 90 or above, stop after 5 tries.
/loop is a command that re-runs a prompt at fixed intervals.
Example: /loop 5m check my PR, address review comments, and fix failing CI
※ Runs locally, so it stops if you shut down your PC
/schedule is a command that runs loops as
"routines" on Anthropic-managed cloud.
※ Runs on the cloud, so it executes even with the PC closed
※ As of July 2026, this is in research preview
Loop components
Osmani lists the components needed for a loop as "5 + memory".
Let's match them with their Claude Code counterparts:
| Component | Role | Claude Code counterpart |
|---|---|---|
| Automations | Start on schedule. Discover and triage work | scheduled tasks · cron, /loop, /goal, hooks, GitHub Actions |
| Worktrees | Isolate parallel agents' work | git worktree, --worktree, subagent isolation: worktree |
| Skills | Externalize project knowledge and procedures | Agent Skills (SKILL.md in .claude/skills/) |
| Plugins / Connectors | Connect to existing tools | plugins, MCP servers |
| Subagents | Separate "the one who makes" from "the one who checks" | subagents (.claude/agents/), agent teams |
| + External memory | State kept outside the conversation (what's done, what's next) | Markdown files (CLAUDE.md, progress files, etc.), external systems like git history |
※ The counterpart column is based on Osmani's article
Incidentally, the primitive form of a "loop" is reportedly the
Ralph loop introduced by Geoffrey Huntley in July 2025:
while :; do cat PROMPT.md | claude-code ; done
It's just a bash loop that continuously feeds the same prompt in a new context each time,
but the idea of "keeping state on the repository side (PROMPT.md, tests) rather than in conversation, and iterating"
is a precursor to today's loop engineering.
Environment
Verification was performed in the following environment:
| Item | Version |
|---|---|
| OS | macOS 26.5.1 (Apple Silicon / arm64) |
| Claude Code | 2.1.201 |
| jq | 1.8.1 (only when using the stop_hook_active guard) |
| Node.js | 20.19.0 (used for Pattern 6 Agent SDK execution) |
| @anthropic-ai/claude-agent-sdk | 0.3.206 |
Setup
All you need is Claude Code.
The demos in this article are complete with just bash.
For Claude Code setup, refer to here.
% claude --version
2.1.201 (Claude Code)
- jq (when using the
stop_hook_activeguard described later)
% brew install jq
Try
Building the loop
Now let's actually build a loop and verify it in Claude Code.
The demo content is as follows:
- Purpose
Create a loop where "even if the agent decides to stop, it can't stop without passing verification" - What we're building
fizzbuzz.shwith an intentional bug (for Claude to fix)test.sh(the definition of "done" — exits with 0 if all pass)- Stop hook verification gate
verify.sh(blocks Claude from stopping as long as tests fail, and feeds back test results)
The expected behavior is as follows:
- Ask Claude to do something unrelated to the tests
- The moment Claude tries to stop, the gate runs the tests
- They fail, so it can't stop
- Claude performs the (unasked-for) bug fix
- Only when all tests pass can it finally stop
The Stop hook (a hook that fires when Claude is about to end its response)
handles the Verify and continue/stop decision parts of the
"Plan → Action → Observe → Verify → continue or stop" closed loop mentioned at the beginning.
※ In the official blog's classification, this is a turn-based loop with a verification gate added
The file structure is as follows. No skills or subagents —
stripped down to just "actual verification" and "stopping condition":
loop-demo/
├── .claude/
│ ├── settings.json # Stop hook configuration
│ └── hooks/verify.sh # Verification gate
├── fizzbuzz.sh # Buggy implementation
└── test.sh # Executable specification (tests)
1. Creating the project
First, prepare "the target for Claude to fix" and "the definition of completion."
A FizzBuzz with an intentional bug, and a test as its executable specification.
% mkdir -p loop-demo/.claude/hooks && cd loop-demo
fizzbuzz.sh (bug: the case for multiples of 15 is missing):
#!/bin/bash
n=$1
if [ $((n % 3)) -eq 0 ]; then
echo "Fizz"
elif [ $((n % 5)) -eq 0 ]; then
echo "Buzz"
else
echo "$n"
fi
test.sh (passing all tests is the definition of completion):
#!/bin/bash
fail=0
check() {
actual=$(./fizzbuzz.sh "$1")
if [ "$actual" != "$2" ]; then
echo "NG: fizzbuzz($1) = $actual (expected: $2)"
fail=1
else
echo "OK: fizzbuzz($1) = $2"
fi
}
check 1 "1"
check 3 "Fizz"
check 5 "Buzz"
check 15 "FizzBuzz"
exit $fail
% chmod +x fizzbuzz.sh test.sh
% ./test.sh
OK: fizzbuzz(1) = 1
OK: fizzbuzz(3) = Fizz
OK: fizzbuzz(5) = Buzz
NG: fizzbuzz(15) = Fizz (expected: FizzBuzz)
% echo $?
1
Right now only the case for 15 is failing.
2. Setting up the verification gate
Next, create the mechanism that "can't stop until tests pass."
The content is just a shell script (.claude/hooks/verify.sh) that runs the tests
and returns an exit code based on the result.
#!/bin/bash
# Stop hook: verification gate that won't let Claude stop until tests pass
cd "$CLAUDE_PROJECT_DIR" || exit 0
RESULT=$(./test.sh 2>&1) && exit 0 # Tests pass → allow stop (end loop)
echo "Tests are failing. Please continue fixing until all tests pass:" >&2
echo "$RESULT" >&2
exit 2 # Block stop → Claude continues working
The hook configuration (.claude/settings.json) is as follows:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/verify.sh"
}
]
}
]
}
}
% chmod +x .claude/hooks/verify.sh
The mechanism follows the official hooks specification exactly:
- The Stop hook executes when Claude is about to end its response
- If the hook exits with exit 2, it "prevents Claude from stopping and continues the conversation" (from the official docs)
- At that time, stderr is passed as feedback to Claude, so if we pass the test results directly, Claude sees "what's failing" and returns to fix it
exit 0 = stopping condition met, exit 2 = verification failed so continue loop.
3. Verifying the hook in isolation
Before integrating with Claude Code, confirm that the gate judges correctly on its own.
※ CLAUDE_PROJECT_DIR is an environment variable set by Claude Code at hook execution time
In the buggy state (will be blocked):
% CLAUDE_PROJECT_DIR="$PWD" ./.claude/hooks/verify.sh
Tests are failing. Please continue fixing until all tests pass:
OK: fizzbuzz(1) = 1
OK: fizzbuzz(3) = Fizz
OK: fizzbuzz(5) = Buzz
NG: fizzbuzz(15) = Fizz (expected: FizzBuzz)
% echo $?
2
With the bug fixed (stop is permitted):
% CLAUDE_PROJECT_DIR="$PWD" ./.claude/hooks/verify.sh
% echo $?
0
Both exit 2 (block + feedback) and exit 0 (stop permitted) behave as expected.
Once confirmed, revert fizzbuzz.sh to its original buggy state.
4. Running it in Claude Code
With everything ready, let's run the loop in Claude Code.
What we ask Claude here is not bug fixing, but document creation: "write a README.md."
% claude
❯ Please write an overview of this process in README.md
If we simply asked "fix the bug," Claude would fix it normally and finish — that's not interesting.
Instead, we give an unrelated request, and confirm that the moment Claude finishes writing the README and tries to stop,
the verification gate fires and the loop forces a bug fix that wasn't requested.
This behavior — "forcing a fix even for an unrelated request" — is a demo design to show how the loop works.
If you put the same hook in an actual development repository,
any request will trigger fixes as long as broken tests exist, and the intended task won't be processed.
In production use, appropriately limit the target tasks, changed files, and test scope.
The actual flow when executed is as follows:
- Claude writes README.md and tries to end its response
- Stop hook runs
test.sh→ fails → blocks stop with exit 2 - The test results in stderr (
NG: fizzbuzz(15) = Fizz ...) are fed back to Claude - Claude can't stop and starts fixing
fizzbuzz.sh - After fixing, tries to stop again → this time all tests pass → exit 0 → session ends
The actual session log is as follows:
❯ Please write an overview of this process in README.md
⏺ First I'll check the contents of the target directory to understand what "this process" refers to.
(omitted: reads files, creates README.md)
⏺ Write(README.md)
⎿ Wrote 62 lines to README.md
⏺ README.md has been created.
(… tries to end response)
⏺ Ran 7 stop hooks
⎿ Stop hook error: [".claude/hooks/verify.sh"]:
Tests are failing. Please continue fixing until all tests pass:
OK: fizzbuzz(1) = 1
OK: fizzbuzz(3) = Fizz
OK: fizzbuzz(5) = Buzz
NG: fizzbuzz(15) = Fizz (expected: FizzBuzz)
⏺ The verification gate (Stop hook) activated, and the stop was blocked until tests pass.
As the gate requires, I'll fix the bug in fizzbuzz.sh.
⏺ Update(fizzbuzz.sh)
⎿ Added 3 lines, removed 1 line
3 -if [ $((n % 3)) -eq 0 ]; then
3 +if [ $((n % 15)) -eq 0 ]; then
4 + echo "FizzBuzz"
5 +elif [ $((n % 3)) -eq 0 ]; then
⏺ Bash(./test.sh)
⎿ OK: fizzbuzz(1) = 1
OK: fizzbuzz(3) = Fizz
OK: fizzbuzz(5) = Buzz
OK: fizzbuzz(15) = FizzBuzz
⏺ All tests passed (exit 0).
This satisfies the gate's passing condition (test success), so I can now stop normally.
Even though we only asked to "write a README," the agent can't stop
unless it passes the verification gate even when it judges it's "done."
In fact, Claude itself recognized
"the verification gate activated and the stop was blocked until tests pass"
and moved on to fix it.
The loop (verification and stopping condition), not the prompt, enforces the defined completion condition —
this is the minimal form of loop engineering.
5. Preventing infinite loops
"Can't stop until tests pass" is also an infinite loop risk.
Claude Code has safety mechanisms built in.
If the Stop hook blocks 8 consecutive times,
Claude Code overrides the hook and ends the turn (since v2.1.143).
※ Officially expressed as "blocks eight times in a row without progress"
If you legitimately need more than 8 iterations,
you can raise the limit with the environment variable CLAUDE_CODE_STOP_HOOK_BLOCK_CAP.
Another safety mechanism is the stop_hook_active guard.
Each time a Stop hook runs, JSON is passed to it, and stop_hook_active within it is
a flag that indicates "is Claude currently running because a Stop hook just stopped it?"
If you keep blocking regardless of this being true,
you risk an infinite loop of "stop → resume → stop again…",
so add an early return at the top of the hook like "if true, exit without doing anything (exit 0)."
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # If already continuing due to a hook, don't block further
fi
※ Our verification gate naturally stops when tests pass, so ↑ this guard isn't needed
Conversely, don't put tasks into this mechanism if you can't write a mechanically determinable stopping condition.
※ The verify.sh in this article is fail-open — it passes with exit 0 if cd fails (simplified for the demo). For strict production use, design it to stop with exit 2.
Building the loop 6 different ways
The Stop hook we just built is one of several ways to implement a loop.
There are other ways to implement loops, so
let me introduce 6 categories of ways to build loops with Claude Code
(Pattern 1 has two forms a / b, so there are 7 examples in total).
| Pattern | Technique | Who verifies | Characteristics |
|---|---|---|---|
| 1a | In-prompt self-verification | The model itself (self-report) | Simplest & most fragile |
| 1b | Ralph loop (bash + claude -p) |
Outer bash (test exit code) | Can be built with zero extensions |
| 2 | /goal |
Separate small model (judges from conversation content) | Built-in. Can be written in one line |
| 2 | /loop · /schedule |
None (launch only. Combine with other patterns for verification) | Time-driven · cloud resident |
| 3 | Stop hook (this article's demo) | Hook (test exit code) | Won't stop until tests pass |
| 4 | Verification skill | The model itself (following provided procedures) | The "knowing what to do" layer |
| 5 | Maker-Checker subagent | A checker in a separate context | Mitigates self-scoring bias |
| 6 | Agent SDK | Outer code | CI / product integration |
The key point to check for these techniques is "at what point does enforcement happen."
1a and 4 depend on the model (probabilistic instructions), while 1b, 3, and 6 enforce from the outside.
2 (/goal) and 5 fall in between, with judgment by a different model or different context.
Now let's build the same loop we just made using each pattern.
Pattern 1a: Describe self-verification in the prompt
The simplest form — use no extensions at all, just instruct "keep fixing until tests pass" within a single run.
This is also a type of loop engineering.
% claude -p "Please fix fizzbuzz.sh so that all ./test.sh tests pass.
Run the tests, fix any failures, and rerun. When all pass, output TESTS_GREEN and finish.
Do not modify test.sh" --allowedTools "Read,Edit,Bash(./test.sh)"
The loop runs only within a single turn's agentic loop, and verification is the model's self-report.
Note that --allowedTools specifies which tools can be used without a permission prompt —
it does not restrict tool usage.
Even in the example above, since Edit is permitted,
manipulation (such as rewriting test.sh to make tests pass trivially) cannot be mechanically prevented.
The countermeasure is a prompt instruction saying "don't modify test.sh,"
so if you want to truly prohibit it, you need to restrict it with --disallowedTools or a PreToolUse hook.
Easy to write, but best suited for trial or throwaway use.
Pattern 1b: Ralph loop (bash while + claude -p)
The minimal autonomous loop run by outer bash.
Known as the Ralph loop,
the difference from Pattern 1a is that the stopping judgment is made mechanically
by the outer ./test.sh (exit code), not by the model's self-report.
#!/bin/bash
# test.sh : run claude -p until tests pass (with a limit)
MAX=5; i=0
until ./test.sh > /dev/null 2>&1; do
i=$((i+1))
[ "$i" -gt "$MAX" ] && { echo "Reached limit of $MAX. Stopping."; exit 1; }
echo "=== Iteration $i/$MAX ==="
claude -p "Run ./test.sh and fix fizzbuzz.sh to make any failing tests pass. Do not modify test.sh" \
--allowedTools "Read,Edit,Bash(./test.sh)"
done
echo "all green"
Since claude -p starts with a fresh context each time,
this approach avoids degradation over long sessions.
State across iterations is held in files (test results in this case) rather than in context.
Suitable for batch use, but requires care like setting iteration limits
and processing in a separate branch/worktree.
Pattern 2: Built-in commands (/goal · /loop · /schedule)
Claude Code has loop commands built in from the start.
To do this demo with /goal, no hooks or scripts are needed:
❯ /goal ./test.sh exits with exit 0. Try 5 turns, stop if not achieved
Each time Claude tries to stop, a small evaluator model
judges whether the condition is achieved from the conversation transcript,
and if not, sends Claude back to work with an explanation.
Since the evaluator can't run tools itself, the key is to define conditions such that
evidence appears in Claude's output, like "the output of running ./test.sh is all OK."
If you state a vague condition, it may become a loop where Claude just reports "achieved" arbitrarily.
For time-driven operation, /loop (re-run locally at fixed intervals) is an option,
and /schedule (research preview) for running in the cloud:
❯ /loop 10m Run ./test.sh and fix fizzbuzz.sh if there are any failures
Note that /loop and /schedule only handle "when to launch" —
they don't have achievement judgment like /goal.
When verification is needed, combine them with in-prompt instructions, Stop hooks, or /goal.
Pattern 3: Stop hook
This is the verification gate we just tried.
Hooks are an enforcement method that doesn't go through the LLM's judgment,
and rules you want to strictly enforce — like "must always pass tests/lint" —
are better placed in hooks than in CLAUDE.md (probabilistic instructions).
Pattern 4: Verification skill (verification skill)
A method of defining verification procedures as a Skill.
Same idea as the verification skill introduced in the official blog —
place a .claude/skills/verify-fizzbuzz/SKILL.md:
---
name: verify-fizzbuzz
description: Always verify changes to fizzbuzz.sh before reporting "complete"
---
Whenever you modify fizzbuzz.sh, always do the following before reporting completion:
1. Run `./test.sh`
2. If there is even one `NG:`, fix it and start over from step 1
3. Confirm all `OK:` output, then report completion
Do not modify test.sh.
Skills are a mechanism for "the model to know what it should do,"
while hooks are a mechanism for "enforcing execution at any given timing."
Combining both gives you a "knows what to do, and is also enforced" configuration.
※ You can also write hooks in the skill's frontmatter
Pattern 5: Maker-Checker (verification-dedicated subagent)
Having the creator grade their own work tends to miss their own bugs (the self-scoring bias mentioned earlier).
So prepare a "verification-dedicated subagent with no Write/Edit permissions"
and have it grade from a separate context.
This is a minimal version of the builder/evaluator pattern shown in
Anthropic's repository (cwc-long-running-agents).
.claude/agents/evaluator.md:
---
name: evaluator
description: A verification-dedicated agent that skeptically reviews whether fixes meet the specification. No editing permissions.
tools: Read, Grep, Bash
---
You review work that another implementer has claimed is "complete."
Do not trust the implementer's self-assessment.
1. Run `./test.sh` and verify the results with your own eyes
2. Read fizzbuzz.sh / test.sh and check for any gaming of the tests (such as rewriting test.sh)
Write only PASS or NEEDS_WORK on the first line of your response,
followed by the reasoning for your judgment on subsequent lines (for NEEDS_WORK, include a list of actionable feedback).
Plausibility is not correctness. If there's no evidence, it's NEEDS_WORK.
Use Bash only for running ./test.sh and read-only operations like ls / cat. Do not modify or create files.
Usage is to simply ask in the main session as follows:
❯ Please fix fizzbuzz.sh. Once you think it's done, have the evaluator subagent
review it, and keep repeating fix and review until PASS is returned.
Since tools doesn't include Write/Edit, the evaluator's role is limited to evaluation.
However, since Bash is permitted, it's not completely read-only.
That's why the prompt limits the use of Bash.
(For mechanical restriction, combine with a PreToolUse hook)
This separation of "the one who makes" and "the one who checks"
mitigates self-scoring bias and improves verification independence.
Pattern 6: Controlling from Code with Agent SDK
To embed the loop in CI/CD or a product, use the Agent SDK.
You can call the same agentic loop as Claude Code as a library.
(TypeScript/Python)
For authentication, if you're automating personally, you can also use a subscription
OAuth token (issued with claude setup-token).
If you're embedding it in a product for external users, an API key or similar is required.
% npm install @anthropic-ai/claude-agent-sdk
% npm install -D tsx
As noted in the comparison table with "verification subject = external code", the key point is
that outside of query() (the inner agentic loop), your own code
executes test.sh to decide whether to continue or stop.
This is the TypeScript version of ralph.sh (Pattern 1b).
loop.ts:
import { query } from "@anthropic-ai/claude-agent-sdk";
import { execFileSync } from "node:child_process";
// Verification is done by external code (not relying on the agent's self-report)
const testsPass = (): boolean => {
try { execFileSync("./test.sh"); return true; } catch { return false; }
};
const MAX = 3;
for (let i = 1; i <= MAX && !testsPass(); i++) {
console.log(`=== Iteration ${i}/${MAX} ===`);
for await (const msg of query({
prompt: "Fix fizzbuzz.sh so that ./test.sh passes. Do not modify test.sh",
options: {
settingSources: [], // Do not load user, project, or local file settings
allowedTools: ["Read", "Edit(./fizzbuzz.sh)", "Bash(./test.sh)"],
permissionMode: "dontAsk",
maxTurns: 10, // Maximum agentic turns per execution
},
})) {
if (msg.type === "result") console.log(msg.subtype); // success / error_max_turns etc.
}
}
if (testsPass()) {
console.log("all green");
} else {
console.error("Limit reached. Human escalation required");
process.exitCode = 1; // Allow CI to treat this as a failure
}
Permissions are restricted via allowedTools so that Edit is limited to fizzbuzz.sh and Bash is limited to running test.sh,
with permissionMode: "dontAsk" (operations not permitted are rejected without confirmation).
Also, the SDK loads the working directory's
.claude/ settings (CLAUDE.md, hooks, etc.) by default.
Since the Stop hook from the main article remains in loop-demo,
running it as-is will cause the gate to trigger inside query() as well.
To make this an independent demo of the "only external code does verification" pattern,
loading is disabled with settingSources: [].
The result of running from a buggy state is as follows.
% npx tsx loop.ts
=== Iteration 1/3 ===
success
all green
In one iteration, fizzbuzz.sh was fixed, and the external testsPass()
confirmed all tests passed (exit 0) and terminated.
If the limit is reached without tests passing, it exits with process.exitCode = 1,
so it can be detected as a failure in CI as well.
Since the external code owns the stop decision, iteration limit, and escalation judgment,
it can be placed directly in CI or a job infrastructure.
In production, consider also using PreToolUse hooks and sandboxes (permissions specification).
The approach to stop conditions, safety measures, and verification is exactly the same as everything covered so far—
only the location changes from shell scripts and configuration files to code.
How to Determine Goals / Stop Conditions in Real Development
The demos so far used a simple "4 tests pass" condition, but
when using loops in real development, the approach is the same:
design by "defining an ambiguous request as a mechanically judgeable stop condition."
The thinking is as follows.
- Reduce to command exit codes, numbers, or counts: Express completion not as "fixed" or "cleaner," but as command success/failure or numerical thresholds
- Make compound conditions a "satisfy all" checklist: List each judgment one by one, such as tests, lint, and type checks (imagine adding verification commands to the main article's
verify.sh) - Always pair with a limit: Attach an iteration count or budget limit, and if not achieved, stop and notify a human
Examples are as follows.
| Ambiguous | Example translation to stop condition |
|---|---|
| Fix the bug | npm test exits with exit 0 |
| Improve code quality | npm run lint, npm run typecheck, and npm test all exit with exit 0 |
| Make the display faster | Lighthouse score is 90 or above (same idea as the official /goal example) |
| Progress library migration | All files in the migration target list are converted and tests pass (= queue is empty) |
Conversely, requests that cannot be mechanically judged, such as "make the documentation easier to read,"
cannot be placed in an autonomous loop as-is.
In that case, it is realistic to make only the judgeable parts—such as "heading structure matches the spec" or "zero broken links"—
the stop conditions, and leave the remaining quality evaluation to a human (or a checker in a separate context).
Also, for large tasks that take a long time to satisfy stop conditions,
rather than trying to complete everything in a single session, it is recommended to
externalize progress to a file like PROGRESS.md and design for handoff across iterations.
Reference: Effective harnesses for long-running agents
Cautions
Loop engineering is powerful, but because it is "a mechanism that iterates automatically," there are also points to be careful about.
Please confirm the following before running.
Cost (Token Consumption)
Loops repeat re-reading, tool execution, and verification with each iteration,
so token consumption will definitely increase.
Anthropic's own measurements also
report that
"agents use about 4 times more tokens than chat, and multi-agent systems use about 15 times more."
Especially in configurations that launch separate contexts, such as Maker-Checker (Pattern 5), costs increase further.
Therefore, take the following measures to avoid consuming more tokens than necessary.
- Set iteration limits (
--max-turns, SDK'smax_turns, budget limits, etc.) - Periodically check consumption (
/usage, etc. See official documentation) - (Pro/Max plans) Set credit limits with
/usage-credits, etc. - When using
claude -p, Agent SDK, or API, consumption may be charged separately, so confirm billing conditions
Preventing the Model from Cheating
If you restrict it to "until tests pass," the model may delete tests, relax conditions,
or cheat to exit the loop.
Take the following measures to ensure the goal is achieved correctly.
- Explicitly state prohibitions like "do not modify test.sh" in the prompt or skill
- Have the checker (evaluator) review the diff to detect tampering with tests
- Use PreToolUse hooks or
--disallowedToolsrestrictions to deny writes to relevant files
The evaluator in cwc-long-running-agents is also designed to
"verify evidence itself from a separate context and return PASS / NEEDS_WORK."
Prompt Injection
Unattended loops read untrusted input such as web pages, issue bodies, and logs,
and execute actual actions.
If malicious instructions are mixed in, there is a risk that the loop will execute them.
The following is not limited to loop engineering, but
take the maximum possible security measures.
- Limit tool permissions to the minimum (reject dangerous operations via hooks or settings)
- Require human approval for irreversible actions such as push, deploy, and external transmission
- Isolate long unattended executions to dedicated branches, worktrees, or sandboxes
The Claude Code official security guide also covers built-in protections against prompt injection and
summarizes best practices, so please refer to it.
Do Not Use for Tasks Where Stop Conditions Cannot Be Written
To repeat: placing a task in an autonomous loop where you cannot write mechanically judgeable stop conditions
tends to result in a meaningless loop that just repeats "completed."
For ambiguous tasks, first have a human run the loop, and only automate once "done" can be defined.
And the responsibility for reviewing and verifying the artifacts output by the loop rests with the human.
Please do not forget that.
Summary
In this article, we organized an overview of loop engineering,
created a minimal autonomous loop with Claude Code's Stop hook,
and confirmed that it actually works.
Loop engineering is a design approach that replaces "the person who prompts an agent"
with "a system that prompts an agent."
It represents the current top floor of the abstraction that has been built up from prompt → context → harness.
The key was not model performance, but the following two points.
- Make verification real: Judge "completion" based on mechanically verifiable grounds like tests
- Design stop conditions: Define completion in a mechanically judgeable form and operate with limits
As demonstrated, these two points can be achieved with just a single Stop hook.
And as seen in the second half, the same principles—from the bash Ralph loop to /goal, verification skills,
Maker-Checker subagent separation, and CI integration via Agent SDK—
can be directly applied across various techniques.
Finally, Addy Osmani's words once more.
Build the loop. But build it like someone who intends to stay the engineer, not just the person who presses go.
The responsibility for reading, verifying, and judging the artifacts output by the loop
remains on the engineer's side, as it always has.
As long as you don't relinquish that responsibility, loop engineering becomes
a powerful tool that replaces "time spent watching over the process"
with "time spent on design and acceptance."
The next time you ask an agent to do something,
why not consider: "Could this task be turned into a loop?"
References
- Loop Engineering — Addy Osmani(2026-06-07) — Article that systematized the terminology
- Peter Steinberger's post on X(2026-06-07) — "You should be designing loops that prompt your agents."
- Getting started with loops — Claude official blog(2026-06-30) — 4 types of loops,
/goal,/loop,/schedule, verification skill - Hooks reference — Claude Code docs — Stop hook exit code behavior and input schema specification
- Automate actions with hooks(hooks guide)— Claude Code docs — Official example of 8-iteration limit and
stop_hook_activeguard - Claude Code CHANGELOG(v2.1.143) — Turn termination after 8 consecutive blocks and introduction of
CLAUDE_CODE_STOP_HOOK_BLOCK_CAP - Create custom subagents — Claude Code docs — Subagent definition, independent context, tool restrictions
- Agent SDK overview — Claude Code docs — Provides the same agent loop as Claude Code as a library
- anthropics/cwc-long-running-agents(GitHub) — Educational material for builder/evaluator loops (explicitly noted as example ingredients, not turnkey)
- Building effective agents — Anthropic(2024-12-19) — Agent definitions, evaluator-optimizer
- Effective harnesses for long-running agents — Anthropic(2025-11-26) — Externalizing state for long-running tasks
- How we built our multi-agent research system — Anthropic(2025-06-13) — Token consumption for agents/multi-agents (approximately 4x/15x)
- Security — Claude Code docs — Built-in protections against prompt injection and best practices for untrusted content
- Manage costs effectively — Claude Code docs —
/usage,/usage-credits, token reduction measures - Harness engineering: leveraging Codex in an agent-first world — OpenAI(2026-02) — Definition of harness engineering
- ReAct: Synergizing Reasoning and Acting in Language Models(Yao et al.) — Theoretical roots of the reasoning + action loop
- The Anthropic leader who built Claude Code says he ditched prompting — now he just writes loops. — The New Stack(2026-06-10) — Overview of the lineage and Cherny's comments
- Ralph Wiggum as a "software engineer" — Geoffrey Huntley(2025-07-14) — The most primitive bash loop
- Designing agentic loops — Simon Willison(2025-09-30) — Theory on agentic loop design (prior consideration before the term "loop engineering" was coined)
- Key takeaways from Boris Cherny on Acquired Unplugged — WorkOS(2026-06-02) — Summary of Cherny's interview