Build the loop, not the prompt.
A small, dependency-light framework for running Claude (Code) in durable, guard-railed loops — so you can hand it a goal or point it at a recurring job and walk away, instead of hand-holding one prompt at a time.
It's the practical distillation of the article "WTF Is a Loop? Peter Steinberger vs. Boris Cherny":
"You shouldn't be prompting coding agents anymore. You should be designing loops that prompt your agents."
A loop here is cron plus a decision-maker in the body: a model looks at the current state, decides the next action, does it, checks whether it worked, and decides whether to keep going — under hard guardrails, with mandatory self-verification, and durable state in git so work survives a crashed container.
- The loop is the product now, not the prompt. The model got cheap; orchestrating it well is the work — and the cost.
- Self-verification is mandatory. "A loop is only as good as its feedback." An open loop with no check is "a machine for generating confident mistakes." → every loop must define a verifier; the framework refuses to run one that doesn't.
- Guardrails are mandatory. Max iterations, no-progress detection, and a dollar/token ceiling. "Without guardrails, you get infinite loops and billing surprises orders of magnitude over budget." → every loop must define a budget ceiling.
- The reusable unit is a named skill, not a re-derived prompt. Loops call sharp skills
(
.claude/skills/); a loop with no reusable skills "is just a while-true around a stranger." - Durable state in git so a crashed run resumes where it left off (Gas Town style).
Plus Boris Cherny's five tips, baked in: auto/headless permissions, multi-agent orchestration, nudge-until-done, run in the cloud, and self-verify end-to-end.
pip install -e . # core (PyYAML only)
pip install -e '.[sdk]' # add the Claude Agent SDK backend
pip install -e '.[dev]' # add pytestRequires the claude CLI on PATH for the cli backend, or the
claude-agent-sdk package for the sdk backend.
automated errand "fix the failing tests in the payments module and push to a branch" \
--verify "make test" --budget 5.00This builds an ephemeral build→test→fix loop, runs it headless (no approval prompts),
checkpoints to git each tick, nudges until make test passes or a guardrail stops it, and
reports back. Add --save loops/fix-payments.yaml to keep it as a reusable loop, or --dry-run
to see exactly what would run without spending a cent.
automated validate loops/autonomous-coder.yaml # schema check (CI-friendly)
automated dry-run loops/autonomous-coder.yaml # show exact invocation + run verifier read-only; NO model spend
automated run loops/autonomous-coder.yaml # go
automated resume loops/autonomous-coder.yaml # continue after a crash / pause
automated resume loops/pr-babysitter.yaml --approve # clear a pending human gate (e.g. a merge)
automated list loops # what's availableZero-Python alternative (bash + claude + jq):
scripts/loop.sh -g "fix the failing tests" -v "make test" -b 5.00 -n 20| Loop | Strategy | What it does |
|---|---|---|
autonomous-coder |
continuous_work | Drive a test suite back to green, one root-cause fix per tick. |
recurring-poller |
interval_poller | Cron-like watcher — check PRs/inbox/deploys on an interval and act only when needed. |
research-pipeline |
pipeline | research → draft → review → output, one stage per tick, judged by an LLM rubric. |
pr-babysitter |
orchestrator | The canonical example: babysit PRs, auto-fix CI, address review comments in isolated git worktrees, never merge without a human gate. |
restore checkpoint → (in-flight? re-drive) → pre-guardrails (budget, iterations)
→ mark in-flight (+commit) → build prompt → run agent (native budget cap = remaining)
→ self-verify (fail-closed) → update cost/tokens/session → composite progress check
→ post-guardrails (budget, no-progress, human gate) → checkpoint (+commit) → stop or continue
- Budget is honored across crashes: cumulative spend lives in the checkpoint, and each tick
passes the remaining budget as the native
--max-budget-usdcap, so a single run is fail-safe even if loop accounting has a bug. (Verified by a test that simulates 20 crashes and asserts spend never exceeds the ceiling.) - No-progress is a composite: it trips only when the git tree is unchanged and the verifier's numeric signal hasn't improved and the agent didn't claim progress — for N consecutive ticks. The loop's own state files are excluded so they don't fake progress.
- Self-verification is fail-closed: a verifier that errors (missing command, crashed test harness, unparseable judge) yields not done, never done.
- Resume persists the model's
session_idso each tick--resumes the agent's own context, and anin_flight_tickmarker lets a restart re-drive a tick that died mid-execution.
name: my-loop
goal: |
Natural-language goal injected into the agent each tick.
strategy:
type: continuous_work # | interval_poller | orchestrator | pipeline
runner:
backend: cli # | sdk | mock
model: claude-opus-4-8
permission_mode: dontAsk # headless default (NOT bypassPermissions)
allowed_tools: [Read, Edit, "Bash(make test*)"]
verifier: # REQUIRED
type: shell # | llm_judge
command: "make test"
signal_command: "..." # prints a number; powers no-progress deltas
guardrails:
max_iterations: 40
budget: { usd_ceiling: 10.00 } # REQUIRED, must be > 0
no_progress: { ticks: 3 }
human_gate: { "on": [merge] }
state:
git: { enabled: true, branch: automated/my-loop, state_path: .automated/my-loop }
worktrees: falsedontAsk+ an explicit tool allowlist is the default — auditable and non-interactive.bypassPermissionsis opt-in (runner.dangerous_bypass: true) and refuses to run as root.- PR/issue/comment text is untrusted input; the
pr-babysitterallowlist excludes force-push, push-to-main, and merge, and routes merges through a mandatory human gate. - Per-loop MCP scoping (
runner.mcp_servers) keeps tool surface and injection risk small.
python -m unittest discover -s tests -t . # 33 tests, all offline (mock runner), no API spendArchitecture: engine.py owns the tick lifecycle; runner/ is the only abstraction over how
Claude is invoked (cli / sdk / mock); strategies/, verifiers/, and guardrails/ are small
pluggable protocols. Everything is runner-agnostic, so the deterministic MockRunner exercises
the entire control flow without a single API call.