Part 2 of 2. Part 1 covered guardrails. Read it first: a loop is only as safe as the harness around it.
You have probably done this. You ask Claude to fix something. The tests fail. You paste the error back. It tries again, fails again, and you paste again. Twenty minutes later it is green, and you spent the whole time as a copy-paste machine.
You were the loop.
A loop hands that job to something outside Claude that keeps saying "not yet" until a check passes. This post is the do-it-yourself version of a workshop I ran for an engineering team: three steps to your first loop, one real use case, and the traps I hit while preparing it.
The Condition Is the Whole Game
A loop is only as useful as its definition of "done." "All tests pass" is something a machine can check. "Make the UI better" is not, and a loop with that goal will run for a long time and produce nothing you want to keep.
If you have done test-driven development, you already know this idea: the failing test defines done, and you go from red to green. A loop is the same thing, with the retries automated.
A good test: imagine handing the task to a brilliant new hire on their first day. If you cannot tell them clearly what "done" looks like, they will loop too. Skill is not the problem. An undrawn finish line is.
Step 1: The Four-Line Loop
Start by seeing the mechanism. claude -p runs Claude once in "headless" mode, without an interactive session: it takes your prompt, does the work, and exits. That exit is what lets an ordinary shell loop wrap it.
Run this in your terminal, inside your repo:
n=0
until [YOUR TEST COMMAND] || [ $n -ge 5 ]; do
claude -p "Run the tests, find the first failing test, and fix only what makes it pass. Do not change the test itself." --allowedTools "Bash,Read,Edit"
n=$((n+1))
done- The done condition is your test command succeeding.
- The cap is five attempts. Never run a loop without a cap.
--allowedToolspre-approves what Claude needs. In headless mode anything else is refused, because nobody is there to click yes.
Two traps I hit:
- Break the code, not the test. If nothing fails, the loop exits immediately. And because the prompt forbids touching tests, a broken test can never pass, so the loop burns all five attempts. Break the code so that a real test fails.
- Check that your test command reports failure. Run
[YOUR TEST COMMAND]; echo $?on a known failure. If it prints0, a wrapper script (often around Docker) is hiding the failure, and the loop can never see it. That is worth fixing anyway.
Step 2: /goal, the Built-In Loop
/goal is the same idea inside Claude Code. Unlike the shell loop, where each run starts fresh, it remembers what it has already tried. You state the finish line, and after every turn a separate, smaller model checks whether it has been reached.
One rule decides whether it works: that checker cannot run commands. It only reads the conversation. So make your condition about something the loop prints. If you want it to stop at 90% coverage, tell it to run the coverage command and show the output every turn.
/goal [a clear, checkable condition about your code], or stop after 8 turnsAlways add "or stop after N turns." Type /goal alone to see its status, and /goal clear to stop it. It pauses on its own only if Claude goes several turns without running any tools. A loop that keeps failing runs until it hits your cap.
Claude Code has a second built-in command, /loop, for a different job: repeating a prompt on a clock. It is ideal for watching something that changes on its own, instead of refreshing a page. Here it checks a pull request every five minutes using gh, GitHub's command-line tool:
/loop 5m "Run gh pr checks [PR NUMBER] and tell me only when all checks pass, or the moment one fails."It keeps running until you ask Claude to cancel it (or seven days pass), so cancel it once you have your answer.
| Tool | Loops until... | Best for |
|---|---|---|
| Shell loop + claude -p | A command succeeds | Long, unattended runs, such as overnight or in CI (the automated build and tests that run on every change) |
| /goal | A condition is judged true | Sessions you are watching |
| /loop | You cancel it | Watching builds, deploys, and pull request checks |
Step 3: A Real Use Case — Raising Test Coverage
Coverage is a great first target: a number a machine can check, on one file or module. It is also a trap. Coverage measures lines executed, not behavior checked. AI-written tests can inflate it by mocking so much that nothing real is tested, by calling code without asserting anything, or by skipping the edge cases that matter.
And a loop cannot grade its own homework. The agent that wrote the tests is the worst judge of whether they are real. You need a check that did not write them.
Here is the version I use, with a reviewer sub-agent (a separate agent with fresh context) as the loop's own gate:
/goal Raise the test coverage of [FILE] to at least [TARGET]% by adding real tests. Each turn, run [COVERAGE COMMAND] and [TEST COMMAND] and show their output. Once the target is reached and all tests pass, spawn a reviewer sub-agent and wait for its result: fresh context, read-only, it reads the tests and [FILE] and lists any test that does not assert real behavior, ending with exactly one line: VERDICT: WEAK TESTS FOUND or VERDICT: NO WEAK TESTS. If weak tests are found, fix them and run the reviewer again. Never declare done yourself. Done only when the reviewer prints VERDICT: NO WEAK TESTS. Stop after 10 turns.Why it is shaped this way: in my first version, the loop declared itself done before the reviewer's report came back. Two changes closed that gap: the verdict is a fixed line that only the reviewer prints, and the loop is never allowed to declare success on its own.
For even more independence, run the review after the loop as a separate claude -p session that is only allowed to read (--allowedTools "Read,Grep,Glob"), or run it in CI.
Before You Start: Readiness and Cost
A loop is likely to work when you have a check a machine can run, a small scope (one file or module, on a branch), your guardrails on, a cap, and evidence the loop can print.
It is likely to fail with no tests, a vague goal, a check that depends on something that can go down, or "the whole app" as the scope. No tests means no definition of done, which means no loop. That is the real reason coverage matters.
By default, a loop has no spending limit. Cap every loop by turns. For unattended headless runs, claude -p --max-budget-usd 5 stops at five dollars. Watch your spend with /usage. On a team plan, the real safety net is a spend limit set by an admin. Start with one capped loop on one file, see what it costs, and then grow.
Bottom Lines
- You have been the loop. Hand the job to a check.
- If a machine cannot check "done," do not loop.
- The checker only sees what is printed. Make the loop show its evidence.
- Every loop gets a cap.
- A loop cannot grade its own homework. Add a check that did not write the work.
For managers: loops do not remove engineers from the work. They move them from approving every step to defining "done" and checking the results. That skill — stating "done" in a way a machine can verify — is worth building on your team regardless of any tool.
References