Iedereen shipt tegenwoordig agents. Er is een productcategorie, een track op elke conferentie, een functietitel.
Daarom maar even hardop wat iedereen stilhoudt: een agent is een loop.
Een model, een paar tools, en een loop die het model blijft aanroepen tot het zegt dat het klaar is. Vier stappen: roep het model aan, voer de tool uit waar het om vraagt, geef het resultaat terug, en nog een rondje. Elke agent die je ooit hebt gebruikt, van een prulscriptje tot een coding agent van miljarden, is die loop met meer tools en een langere system prompt.
Maar dit is het stuk dat voor jou telt. Gebruik je Claude Code, dan zit je al middenin die loop. Je schrijft hem meestal niet. Je stuurt hem. En je kunt hem behoorlijk hard sturen zonder ook maar een SDK aan te raken.
De loop is het makkelijke deel. Weten wanneer hij moet stoppen is het echte werk, en dat staat op geen enkele slide.
Je zit al in de loop
Open Claude Code, typ een verzoek, en kijk hoe hij bestanden leest, commando’s draait, code aanpast, de tests draait. Dat zijn de vier stappen, beurt na beurt, tot hij besluit dat hij klaar is.
"Tot hij besluit" is de zwakke plek. Het model heeft geen zicht op waar het heen gaat. Het voorspelt het volgende token, en dan weer een, en dan weer een. Het kan niet naar zijn eigen output kijken en die toetsen aan de werkelijkheid. Ik schreef eerder over die blindheid: er is niemand thuis.
Laat dat ding nu beslissen wanneer de loop eindigt. Een model dat niet ziet waar het heen gaat, met de opdracht door te gaan tot het zich klaar voelt, meldt vol vertrouwen dat een taak af is die het nooit heeft afgemaakt. Elke keer weer. De loop lost de blindheid niet op. Hij versterkt hem, want elke beurt bouwt op de vorige.
Het echte werk is dus drie dingen aan de loop vastschroeven: een stopconditie die een machine kan controleren, echte feedback bij elke beurt, en een harde grens zodat hij niet eeuwig doorgaat. Het mooie is: Claude Code geeft je alle drie zonder code.
De zwakke manier: vriendelijk vragen
Het eerste wat mensen proberen is prompten. "Ga door tot alle tests slagen. Draai ze na elke wijziging."
Dat werkt soms, en het is het proberen waard. Maar wat controleert of de tests slagen, is hetzelfde model dat zijn eigen werk niet kan verifiëren. Het zegt dat de tests groen zijn omdat dat het waarschijnlijke volgende token is na twintig beurten proberen, niet omdat het iets heeft gedraaid. Vriendelijk vragen zet het model aan het roer van zijn eigen stopconditie, precies het stuk waar het slecht in is. Het bredere punt tegen vriendelijk vragen heb ik elders al gemaakt.
Je wilt dat iets anders dan het model beslist wanneer de loop stopt.
De sterke manier: een Stop hook
Claude Code vuurt een Stop hook af zodra het model denkt dat het klaar is. En een Stop hook mag weigeren.
Stopt je hook met exit code 2, dan wordt Claude tegengehouden. Wat je naar stderr schrijft, krijgt hij terug als reden, en de loop gaat door. Dat is een loop-tot-groen die jij bepaalt, afgedwongen door de harness in plaats van door het optimisme van het model:
#!/usr/bin/env bash
# .claude/hooks/loop-tot-groen.sh
if ! npm test --silent; then
echo "De tests falen nog. Ga door tot ze slagen." >&2
exit 2 # blokkeert het stoppen; Claude leest stderr en werkt door
fi
exit 0 # tests slagen: laat hem stoppenHang hem in .claude/settings.json:
{
"hooks": {
"Stop": [
{ "hooks": [{ "type": "command", "command": ".claude/hooks/loop-tot-groen.sh" }] }
]
}
}(Heb je liever JSON dan exit codes, dan kan de hook {"decision": "block", "reason": "..."} naar stdout printen. Zelfde effect.)
Nu is de stopconditie een commando, geen wens. Het model praat zich niet langs een falende npm test, want de poort vraagt niet naar zijn mening. Dit is dezelfde deterministische grip waar ik bij hooks maar over blijf doorzeuren: stop met suggereren, begin met garanderen.
Eén ding om te weten: een Stop hook die altijd blokkeert, is een oneindige loop. De input van de hook bevat een stop_hook_active-vlag die true is wanneer Claude al doorgaat vanwege een stop hook. Lees die, of tel de beurten in een tijdelijk bestand, en stop er na een redelijk aantal mee. Een loop waar je niet uit komt is erger dan geen loop.
De ingebouwde manier: /goal en /loop
Je hoeft niet altijd een hook te schrijven. Twee commando’s dekken het meeste.
/goal stelt een eindconditie in en werkt autonoom door, beurt na beurt, tot eraan is voldaan. Onderweg houdt het beurten, tokens en verstreken tijd bij. Het detail dat het meer maakt dan een prompt: na elke beurt controleert een apart, kleiner model of er echt aan de conditie is voldaan. De schrijver is niet ook de beoordelaar. Dat gat in de verificatie is voor je gedicht.
/goal alle tests onder tests/auth slagen en de linter is schoonGeef het een conditie die de beoordelaar daadwerkelijk kan inschatten op basis van wat in beeld staat, dezelfde discipline als bij een Stop hook, alleen in gewoon Nederlands.
/loop is de andere vorm: draai een prompt of commando op een vast ritme, steeds opnieuw. Geef hem een interval, of laat hem zelf het tempo bepalen en per ronde een pauze kiezen op basis van wat hij ziet.
/loop 5m check CI op de open PR en geef een seintje als hij rood wordtDat is de loop naar buiten gericht, op iets dat met de tijd verandert, in plaats van naar binnen op een taak die af moet.
En waaiert het werk uit in plaats van dat het itereert, start dan subagents: meerdere loops parallel, elk op een eigen stukje, de resultaten aan het eind verzameld.
Je eigen loop bouwen
Bouw je een product in plaats van Claude Code te sturen, dan schrijf je de loop zelf. Het blijft bij tien regels. Tegen Claude, in TypeScript, zonder framework:
let messages: Anthropic.MessageParam[] = [{ role: "user", content: task }];
while (true) {
const response = await client.messages.create({
model: "claude-opus-4-8",
max_tokens: 16000,
tools,
messages,
});
if (response.stop_reason === "end_turn") break; // model zegt dat het klaar is
messages.push({ role: "assistant", content: response.content });
const results = [];
for (const block of response.content) {
if (block.type === "tool_use") {
const output = await runTool(block.name, block.input);
results.push({ type: "tool_result", tool_use_id: block.id, content: output });
}
}
messages.push({ role: "user", content: results });
}Kijk naar de exit: end_turn betekent dat het model beslist. Je hebt net hetzelfde probleem geërfd dat Claude Code oplost met hooks en /goal, alleen is het nu aan jou om op te lossen. De API geeft je er knoppen voor. Task budgets (beta) geven het model een token-budget voor de hele loop, zodat het netjes kan afronden in plaats van eindeloos te malen. En while (true) mag nooit letterlijk zijn: begrens de beurten, en maak luid kenbaar dat je tegen het plafond loopt in plaats van te doen alsof onaf werk af is.
let turns = 0;
while (turns++ < 25) { /* ... de loop ... */ }Feedback geeft de loop ogen
Eén ding loopt door dit alles heen, of je nu in Claude Code zit of in de SDK.
Bij elke beurt krijgt het model terug wat je tools teruggeven. Geef het rijk, echt signaal, dan kan het bijsturen: de test noemt de assertion die faalt, de type checker wijst de regel aan. Geef niets bruikbaars terug, of laat het model zijn eigen huiswerk nakijken, en je hebt een echokamer met een runtime gebouwd.
Daarom draait de Stop hook de echte tests, daarom gebruikt /goal een aparte beoordelaar, daarom moet jouw runTool de echte exit code en stderr teruggeven, falen en al. Hoe zelfverzekerd het proza van het model klinkt, zegt je niets. De exit code zegt je alles.
Wat je echt moet doen
- Definieer "klaar" als een commando, niet als een wens. Een Stop hook, een
/goal-conditie, een test-poort. Kun je de check niet schrijven, dan kan de loop het ook niet weten. - Geef bij elke beurt echte resultaten terug. Laat het model nooit het enige zijn dat het model beoordeelt.
- Zet er een harde grens op.
stop_hook_active, een beurtenteller, een token-budget. Laat het luid falen bij het plafond, niet stilletjes wanneer het opgeeft. - Controleer de output toch zelf. Een groene loop is niet hetzelfde als correct werk. Ship nooit code die je niet snapt, hoe goed de tests er onderweg ook uitzagen.
Niets hiervan is exotisch. Het is het roemloze steigerwerk rond een loop, grotendeels een hook-bestand en een slash-commando, en het is het hele verschil tussen een agent die werk levert en een agent die de schijn van werk ophoudt tot je wegkijkt.
De loop was nooit het slimme deel. Iedereen kan while (true) schrijven. Het vakmanschap zit in de conditie waarmee je break mag schrijven.