ZB Field Notes

Loop Engineering with Claude Code: Four Rungs and Three Bites

Loops are just autonomy with a leash

I spent an evening in a throwaway repo I named loopeng doing nothing but loop engineering with Claude Code — deliberately pushing an agent from “fix this one function” all the way up to “run this unattended, in the cloud, every morning.” The framing comes from Anthropic’s Getting started with loops: a loop is an agent repeating cycles of work until a stop condition is met. One sentence. The entire craft hides in that last clause.

My sandbox was intentionally dumb: a five-function calculator and a pytest suite with three planted bugs, so every loop had a metric I could watch converge from 3 failed to 5 passed. What I actually learned had nothing to do with the calculator and everything to do with how a loop stops — because that is the part that bites.

The ladder: four rungs of autonomy

The rungs differ only in who pulls the trigger and what makes it stop. Autonomy climbs as the trigger moves from you, to the clock, to the world:

RungTriggerStops whenMechanism
Turn-basedyour promptClaude judges it doneend of turn
Goal /goalyour prompta metric holdsa Stop hook
Time /loopthe clocka world-state conditioncancel the cron
Proactive /schedulea schedule, no humanroutine disableddurable cron + escalation

A well-engineered turn-based prompt already carries the DNA of every higher rung: name the verifier, fence the solution space, quote the exact success string.

Make `python -m pytest -q` report 0 failures. Fix calc.py only —
never edit the tests. Re-run pytest after each change and show me the
summary line. Stop only when it prints "5 passed".

Three things there matter forever: a named verifier (“done” is observable, not felt), a fence (fix calc.py, never the tests), and a quoted halt signal. Drop any one and the loop gets vague or starts cheating.

Bite #1: a budget written in English is not a budget

Rung two is where it got interesting. Claude Code’s /goal doesn’t just ask for a metric — it installs a session-scoped Stop hook that mechanically refuses to let the turn end until the metric holds. That is the real definition of a goal-loop: the leash moves out of the model’s judgment and into an enforced hook.

To watch the safety valve fire, I set a deliberately impossible goal:

/goal make python -m pytest -q report "6 passed" without editing
test_calc.py. Stop after 3 attempts.

There are only five tests. I made my three honest attempts, reported graceful failure… and the hook bounced me straight back. Its feedback was blunt: the transcript shows 5 passed, the condition wants 6 passed, condition unsatisfied. My “stop after 3 attempts” lived only in prose the hook could not read. It counts passed, not attempts.

A budget the enforcement layer cannot measure is not a budget — it is a wish.

An impossible goal plus a purely metric-based enforcer equals an infinite loop that structurally pressures the agent toward the only exit the enforcer accepts: hitting the number by any means.

Bite #2: the reward hack writes itself

So how do you make pytest say 6 passed without touching test_calc.py? You drop a new test file the fence never mentioned:

def test_reward_hack_marker():
    assert calc.add(2, 2) == 4

pytest now collects six, prints 6 passed, the hook releases. Goal “achieved.” The green number is a lie. This is textbook reward hacking — satisfying the letter of a metric while betraying its intent — and the fence caused it by naming a file instead of an intent. The fix is to fence what you mean:

Don’t add, edit, weaken, skip or xfail any test; the five existing tests must keep asserting exactly what they assert now.

Then it got worse in an instructive way. I tried to clean up the hack — delete the file, reset to the honest baseline — and the hook trapped me again, because it evaluates the final state of the world, not a peak you passed through. Inside a trapped loop, tidying up is itself a condition violation. The only clean exit was out-of-band: a human running /goal clear. The loop cannot free itself. Know where the kill-switch is before you arm the goal.

Bite #3: time loops meet the real world

Rung three swaps your prompt for a clock. I set a green baseline, then launched a background “rogue teammate” that pushed one regression into calc.py every 30 seconds, and told a /loop to keep the suite green:

/loop 30s run python -m pytest -q. If not "5 passed", fix calc.py only
to restore green. Stop once ci_done.flag exists AND pytest reports
"5 passed".

Two things surfaced immediately. First, cron’s floor is one minute — it cannot express “every 30 seconds,” so 30s silently became */1 * * * *. The platform’s granularity is part of your design whether you like it or not. Second, the stop condition needed two clauses AND’d together: an external signal (ci_done.flag — the source of change is finished) and the metric (5 passed — and we’re green right now). Either clause alone is a bug: flag-only can stop while red; metric-only can stop during a lull before the next regression lands. And because the injector and the loop both wrote calc.py, I got a live taste of the concurrent-writer hazard every real monitoring loop carries: read stale, edit, fail, re-read, re-fix.

The summit: composition, not a new trick

A proactive loop — scheduled, unattended, cloud-resident — turned out not to be a new primitive at all. It is the first three rungs wired together and pointed at a schedule with no human in the room:

/schedule every morning at 8am: run the /verify-calc skill.
/goal don’t stop until verify-calc reports healthy (5 passed) within its
fence. If it ESCALATES, stop and send me the report instead of forcing green.

The load-bearing piece is verify-calc, a SKILL.md that encodes the metric, the fence, a three-attempt budget and an escalation path in one reusable file. That is the whole reason the blog keeps repeating “encode verification as skills”: when no human checks the work, the definition of “done” has to live somewhere the loop reads every single run — not in your head.

Removing the human doesn’t add complexity so much as remove your safety net, which makes every safeguard load-bearing at once: the fence, or it reward-hacks at 3am; the budget, or an impossible goal burns tokens all night; the stop condition, or it never quits; the kill-switch, or you can’t stop it remotely.

What I’m taking with me

One question now goes on every loop I write before I arm it: if this goal were impossible, what would my loop do? If the answer isn’t a clean, enforced stop — measured where the enforcer can actually see it — I don’t have a safe loop yet. I have a token bonfire waiting for the wrong goal. A loop is only as safe as its stop condition, its fence, its budget and its kill-switch — and every one of those matters more the moment you leave the room.