Loop Engineering — Building Autonomous Loops with Claude Code

Loop Engineering — Building Autonomous Loops with Claude Code

An explanation of "Loop Engineering," an evolved form of AI agents. We introduce in detail the automatic iterative cycle of Plan-Action-Observe-Verify, the four types of loop classifications in Claude Code, six implementation patterns, and methods for designing verification and stopping conditions.
2026.07.10

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:

  1. 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."

  1. 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_active guard 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
    1. fizzbuzz.sh with an intentional bug (for Claude to fix)
    2. test.sh (the definition of "done" — exits with 0 if all pass)
    3. 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:

  1. Ask Claude to do something unrelated to the tests
  2. The moment Claude tries to stop, the gate runs the tests
  3. They fail, so it can't stop
  4. Claude performs the (unasked-for) bug fix
  5. 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:

  1. Claude writes README.md and tries to end its response
  2. Stop hook runs test.sh → fails → blocks stop with exit 2
  3. The test results in stderr (NG: fizzbuzz(15) = Fizz ...) are fed back to Claude
  4. Claude can't stop and starts fixing fizzbuzz.sh
  5. 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.

  1. Reduce to command exit codes, numbers, or counts: Express completion not as "fixed" or "cleaner," but as command success/failure or numerical thresholds
  2. 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)
  3. 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's max_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 --disallowedTools restrictions 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


Claudeならクラスメソッドにお任せください

クラスメソッドは、Anthropic社とリセラー契約を締結しています。各種製品ガイドから、業種別の活用法、フェーズごとのお悩み解決などサービス支援ページにまとめております。まずはご覧いただき、お気軽にご相談ください。

サービス詳細を見る

Share this article