Module 11b · 45 min

Loops and Goals

Pick the driver that keeps a session working (a condition, a clock, or your own script) and write a goal condition an evaluator can actually judge.

Surface
/goal, /loop, .claude/loop.md

You set a task going, step away, and come back to find Claude stopped three turns in, waiting for you. Another day the opposite happens: it kept going long after the work was done, spending turns on nothing. Both are the same question answered two different ways, when a turn ends, something has to decide whether there is another one, and the whole of this page is about who that something is.

Module 42 pulled deciding what to do apart from doing it. This page pulls apart a different pair: finishing a turn, and ending the session. Claude Code gives you three ways to keep a session running turn after turn, and the goal page ships the comparison itself.

Who starts the next turn

The three drivers differ by one thing only: what event starts the next turn. Here is the table as the goal page writes it.

ApproachNext turn starts whenStops when
/goalThe previous turn finishes, or an idle check-in comes due while background work keeps the goal waiting, up to three times per goal between your promptsA model confirms the condition is met or judges it impossible, or a turn fails on an error you have to fix, or you run /goal clear
/loopA time interval elapsesYou stop it, or Claude decides the work is done
Stop hookThe previous turn finishesYour own script or prompt decides

A condition, a clock, your own script. That is the whole menu. The goal page is blunt that there is no fourth in-session driver hiding behind these: the three rows above are the complete documented set, and there is no run-until-done switch beyond them.

One thing all three build on top of, rather than replace, is auto mode, the permission mode from module 11 where a classifier approves tool calls instead of asking you. The goal page draws the line between the two jobs:

Auto mode on its own approves tool calls within a single turn but doesn’t start a new one. Claude stops when it judges the work done. /goal adds a separate evaluator that checks your condition after every turn, so completion is decided by a fresh model rather than the one doing the work. The two are complementary: auto mode removes per-tool prompts, and /goal removes per-turn prompts.

Auto mode decides whether a tool runs inside a turn. The three drivers decide whether there is a next turn at all. You will usually want both: auto mode so the turns run unattended, and a driver so they keep coming.

A condition decides: /goal

Run /goal followed by a condition and Claude keeps working toward it without you prompting each step. Setting it starts a turn immediately with the condition as the directive, and a ◎ /goal active indicator shows how long it has been running.

/goal all tests in test/auth pass and the lint step is clean

What makes a goal different from a long prompt is the evaluator. An evaluator is a second model that reads the conversation after each turn and returns a verdict on your condition. The goal page states what it is underneath:

/goal is a wrapper around a session-scoped prompt-based Stop hook. Each time Claude finishes a turn, Claude Code sends the condition and the conversation so far to your configured small fast model, which defaults to Haiku on the Claude API.

That one sentence carries most of the module. A goal is a Stop hook, the program from module 9 that runs when Claude tries to end its turn. It is session-scoped, meaning it lives in this conversation and no other, and one goal can be active at a time. It runs a prompt rather than a script, so a fresh model judges the condition instead of a fixed piece of code. Because it is a hook, it is unavailable exactly where hooks are: the goal page ties it to the same workspace-trust rule as hooks in settings files, and turns it off when disableAllHooks is true or allowManagedHooksOnly is set.

The evaluator returns one of three verdicts, each with a short reason:

VerdictWhat Claude Code does
Not yet metClaude keeps working and takes the reason as guidance for the next turn
MetClaude Code clears the goal and records an achieved entry in the transcript
ImpossibleThe evaluator judged the condition can never be satisfied; the goal clears and a failed entry is recorded with the reason

Two safety valves sit under that. If Claude keeps answering the evaluator without making progress, meaning no tool use for several turns in a row, Claude Code stops the loop, prints a warning, and hands control back to you with the goal still set. And four kinds of failure clear the goal outright rather than retrying: an authentication failure when Claude Code manages its own credentials, an exhausted credit balance, a context overflow that auto-compaction could not clear, and a model that is not available. The warning starts with Goal cleared after an unrecoverable error and ends with Run /goal again to continue. Any other failure, including transient ones such as rate limits, leaves the goal active.

Background work changes the rhythm. If a subagent or a background shell command is still running when a turn ends, Claude Code skips that turn’s evaluation and checks at the end of the next turn that finishes with nothing running. Once background work has kept the goal waiting for 30 minutes, a check-in comes due, and later check-ins back off, waiting twice as long each time up to four times the first interval. In an interactive session Claude Code will start a turn on its own to deliver a check-in, but no more than three idle check-ins per goal between your prompts, and the third one says idle check-ins are paused until you send another prompt. Set CLAUDE_CODE_GOAL_CHECKIN_MINUTES to change the first interval, or to 0 to turn check-ins off.

Write a condition the evaluator can judge

The evaluator does not run commands and does not read files. It judges your condition against what Claude has already put into the conversation. The goal page is direct about what that means for how you phrase it:

The evaluator judges your condition against what Claude has surfaced in the conversation. It doesn’t run commands or read files independently, so write the condition as something Claude’s own output can demonstrate.

So a condition that holds up over many turns has three parts: one measurable end state such as a test result or an empty queue, a stated check for how Claude should prove it such as “npm test exits 0” or “git status is clean”, and any constraint that must not change on the way there such as “no other test file is modified”. The condition can run to 4,000 characters. To bound how long it runs, add a clause like or stop after 20 turns, and Claude reports progress against it each turn. While the goal is active, the transcript shows each verdict, and Ctrl+O opens the reason behind it.

A goal does not change your permission mode. To let its turns run without prompts, set it while you are in auto mode. It also works headlessly: /goal reaches a headless run, one with no interactive terminal, only through the prompt string, and there is no --goal flag.

claude -p "/goal CHANGELOG.md has an entry for every PR merged this week"

A clock decides: /loop

/loop is the second driver, and it is a bundled skill rather than a built-in command, a prompt Claude Code hands to Claude, not a harness feature. What you pass it decides how it behaves.

What you provideExampleWhat happens
Interval and prompt/loop 5m check the deployYour prompt runs on a fixed schedule
Prompt only/loop check the deployYour prompt runs at an interval Claude chooses each iteration
Interval only, or nothing/loopThe built-in maintenance prompt runs, or your loop.md if one exists

A fixed interval is converted to a cron expression and scheduled. Cron is the standard way to say “run this on a repeating clock”, written as five fields. The units are s, m, h, and d; seconds round up to the nearest minute because cron has one-minute granularity, and an interval that does not map to a clean step, such as 7m or 90m, is rounded to one that does, with Claude telling you what it picked. Omit the interval and Claude self-paces instead, choosing a delay between one minute and one hour based on what it saw, printing the delay and its reason at the end of each iteration. Self-pacing runs through the ScheduleWakeup tool, which Claude calls at the end of each iteration to set the next one, or calls with stop: true to end the loop.

Omit the prompt as well and /loop runs a built-in maintenance prompt: continue unfinished work from the conversation, tend the current branch’s pull request, and run cleanup passes when nothing else is pending. It does not start new initiatives, and irreversible actions such as pushing proceed only when they continue something the transcript already authorized.

The one file Claude Code looks for here

To replace that built-in prompt with your own, create a loop.md. It is the single file in loop-and-goal territory that Claude Code reads on its own, and it defines one default prompt for a bare /loop, not a list of tasks.

PathScope
.claude/loop.mdProject-level. Takes precedence when both files exist.
~/.claude/loop.mdUser-level. Applies in any project that does not define its own.

It is plain Markdown with no required structure: write it as if you were typing the /loop prompt directly. Edits take effect on the next iteration, so you can refine it while a loop runs. Content past 25,000 bytes is truncated.

Two hard bounds keep a forgotten loop from running away. A recurring task expires 7 days after it is created, firing one final time and then deleting itself. And a session holds at most 50 scheduled tasks at once, each with an 8-character ID you can cancel through the CronDelete tool. There is also jitter: to keep every session from hitting the API at the same wall-clock moment, the scheduler offsets recurring fires by up to 30 minutes (or half the interval for sub-hourly tasks), with the offset derived from the task ID so it is stable. Setting CLAUDE_CODE_DISABLE_CRON=1 turns the scheduler off entirely, and /loop along with it.

To stop a self-paced /loop while it waits for the next iteration, press Esc, which clears the pending wakeup. A fixed-interval loop keeps running until you cancel it like any other scheduled task or the seven days elapse, so Esc is not the tool for that one.

For scheduling that outlives the session, the work moves off /loop entirely.

Where it runsNeeds the session open?Survives a restart?
/loopYour machineRestored on --resume if unexpired
Desktop scheduled taskYour machineYes
RoutineCloud, research previewYes

A routine runs in the cloud on a cron schedule with no open session and no permission prompts. The routines page marks the whole feature research preview, “Behavior, limits, and the API surface may change”, so this course states it exists and stops there. It carries one warning worth reading before you trust any unattended run, this one included.

A green run is not a finished task

The routines page, on what a green status in the run list means:

A green status in the run list means the session started and exited without an infrastructure error. It does not mean the task in your prompt succeeded. Open the run to read the transcript and confirm what Claude actually did. Blocked network requests, missing connector tools, and task-level failures all surface there rather than in the status indicator.

Your own script decides: the Stop hook

The third driver is the one you already built. In module 9 the hooks lab’s tests_must_pass.py is a Stop hook that reads stop_hook_active first, then runs the suite and blocks the turn from ending while it is red. That is a loop driver: it decides, from your own code, whether Claude gets another turn. Nothing new to write here, the point is to see it as the same shape as the other two, with the decision moved into a script you own.

The hooks guide is where the loop-driver Stop hook is documented for a reader, and it is also where the safety limit lives.

Claude Code overrides a Stop hook after it blocks eight times in a row without progress. Your hook script needs to check whether it already triggered a continuation. Parse the stop_hook_active field from the JSON input and exit early if it’s true.

If your hook legitimately needs more than eight iterations to converge, raise the cap with CLAUDE_CODE_STOP_HOOK_BLOCK_CAP.

That is the half of the fact tests_must_pass.py depends on and the repo’s own notes half-record: the continuation caps at eight blocks, and CLAUDE_CODE_STOP_HOOK_BLOCK_CAP raises the cap when a real check needs more rounds to pass. A script Stop hook makes a deterministic decision. Where you want judgment instead, the hooks guide’s prompt-based Stop hook hands the “are we done” question to a model, which is exactly the mechanism /goal wraps.

Four rungs, from a prompt to a second model

Line the drivers up and they form a ladder. The best practices page draws it in its section on giving Claude a way to verify its work: once a check exists, you choose how hard it gates the stop.

RungThe check runsWho grades it
In one promptYou ask Claude to run the check and iterate in the same messageThe model doing the work
Across a session (/goal)A separate evaluator re-checks the condition after every turnA fresh model
A deterministic gate (Stop hook)Your script runs the check and blocks the turn until it passesYour code
A second opinionA verification subagent tries to refute the result in a fresh contextA model that did not do the work

The best practices page names the tradeoff so you do not have to guess at it: “Each step trades setup for attention. The prompt version works on any task today. The /goal and Stop hook versions are what let an unattended run finish correctly without you.” The rung to reach for climbs with how long the run goes unwatched. It also gives the standing instruction that makes every rung usable: have Claude show evidence, the test output or the command and what it returned, rather than asserting success.

The fourth driver is your own process

Everything above keeps one session running. Step outside the session and there is one more driver, and it is you. The best practices page shows the shape: a shell loop that calls claude -p once per item, each invocation its own short-lived run.

for file in $(cat files.txt); do
  claude -p "Migrate $file from Python 2 to Python 3. Return OK or FAIL." \
    --allowedTools "Edit,Bash(git commit *)"
done

The loop is a plain for. What starts each iteration is not a condition, a clock, or a hook, it is the next line of your script. --allowedTools scopes what each run may do, which matters when the run is unattended. Writing that driver well, capping its turns and dollars and reading a schema-validated object back instead of grepping prose, is the whole subject of module 21. This page stops at naming it as the fourth place the decision can live.

Watch a loop fire, then aim a goal

Start with the clock, because it is the one you can watch. In a real project, run a self-paced loop with a prompt and no interval:

/loop check whether CI passed and address any review comments

Claude runs the prompt, then prints the delay it chose and why before waiting. It fires again when that delay elapses. While it is waiting, press Esc. The pending wakeup clears and the loop does not fire again. That stop works because the loop is self-paced; a /loop 5m ... on a fixed clock would keep its schedule until you cancel the task.

Now the condition. Write a goal that has all three parts the recipe calls for, an end state, a stated check, and a bound:

/goal every file in src/models has a matching test in test/models,
npm test exits 0, no file outside test/models is modified,
or stop after 20 turns

Setting it starts a turn at once. After each turn the evaluator returns Not yet met, Met, or Impossible, and the transcript shows the verdict. Press Ctrl+O on a verdict to read the reason the evaluator gave, which is also the guidance Claude takes into its next turn. You are watching a second model grade the first one, once per turn, against words you wrote.

Build

Two artifacts, both in a repository you actually work in.

First, a loop.md. Create .claude/loop.md and write the maintenance prompt you would want a bare /loop to run on this repo. The scheduled-tasks page gives one to adapt:

Check the `release/next` PR. If CI is red, pull the failing job log,
diagnose, and push a minimal fix. If new review comments have arrived,
address each one and resolve the thread. If everything is green and
quiet, say so in one line.

Replace it with a chore that is real for your project. Then confirm Claude Code found the file: run a bare /loop with no prompt and no interval, and watch it run your text instead of the built-in maintenance prompt. If it runs the built-in prompt instead, the file is in the wrong place, the project path is .claude/loop.md, and a ~/.claude/loop.md in your home folder is used only where no project file exists.

Second, one /goal for a real chore. Pick something with a check Claude can run and surface, write the condition with an end state, a stated check, and an or stop after N turns bound, and set it in auto mode so the turns run without prompts. Watch the verdicts. When it clears, run /goal with no argument to see the achieved condition, its duration, and the turn count.

The mistake most people make first

You write a goal that names the end state and forgets the check:

/goal the authentication module is fully migrated to the new API

It looks complete. It runs, and after a turn or two the evaluator marks it Met, and you walk away satisfied. Then the tests fail in CI.

Here is why. The evaluator reads only the transcript. It runs no commands and opens no files. “Fully migrated” is a claim, and the only evidence it has is Claude’s own words, so when Claude writes “the migration is complete” the evaluator has a plausible-looking transcript that says the condition holds, and it agrees. Nothing checked reality, because you never told it what checking reality would look like.

The fix is the stated check. Add the thing Claude’s output has to demonstrate:

/goal the authentication module is migrated: npm test exits 0 with
every test in test/auth passing, and no file outside src/auth changes,
or stop after 30 turns

Now “Met” requires a test run in the transcript with a zero exit, which the evaluator can read. The condition became judgeable the moment it named its own evidence.

This is one lesson wearing three costumes across the course. In module 9, exit 0 from a hook is no decision, not approval. On the routines page above, a green run means the session exited, not that the task succeeded. Here, a “done” with no check is not done. A zero is not a yes, wherever you meet it.

Does this travel?

Split it by piece, because the pieces move at different speeds.

The evaluator and the cron scheduler are Claude Code machinery. A goal’s second-model grading and /loop’s scheduling tools have no counterpart you can assume elsewhere, and curriculum/research/05-cross-harness-portability.md does not survey /loop or /goal in any other harness. That is a gap in our research, not a finding that other tools lack them. Until someone fetches those docs, treat any claim about another tool’s built-in goal or loop as unverified.

Two pieces do travel. /loop is a bundled skill, which means its behavior is a prompt and loop.md is plain Markdown, so the idea of a project-defined default loop moves anywhere you can hand a model a prompt, even where the scheduling around it does not. And the Stop-hook driver rides on hooks, which module 9 found in seven of ten surveyed harnesses: wherever a harness fires a lifecycle event you can block, you can write your own “keep going until the check passes” loop, whatever that harness calls the event. That is it.

Check yourself

  1. You set a /goal and it is marked Met, but the work is not actually done. Name what the evaluator can and cannot see, and the one clause you would add to the condition to close the gap.
  2. You want to stop a running /loop. When does pressing Esc end it, and when does it do nothing?
  3. A Stop hook keeps sending Claude back to work and the turn ends with a warning about blocking too many times. What is the limit, what field should the hook have read to avoid hitting it, and how would you raise the limit if the check genuinely needs more rounds?