Blog
Making an AI agent improve your system without letting it grade its own homework.
I build RIFF, a platform where businesses get AI phone receptionists. At some point I stopped asking Claude to improve it and started asking Claude to build a system that improves it — a loop that picks a weakness, fixes it, measures before and after, and keeps the change only if the numbers moved. Getting that to work honestly — without the agent gaming its own scores, leaving half-built experiments everywhere, or "improving" things nobody can verify — turned out to be a design problem with teeth. This is the write-up of what survived contact with reality, plus two installable Claude skills (a beginner version and the full version) so you can run one yourself.
A self-improving loop is an agent session that repeatedly picks a weakness in a system, fixes it, and measures whether things got better. The naive version of this fails in predictable ways: the agent improves things it can't measure (so nothing is proven), thrashes between goals, quietly relaxes its own grading standards, leaves half-built experiments everywhere, and after ten sessions nobody can say whether the system is better or just different.
The ratchet is the mechanical fix. A ratchet moves in one direction and locks: every cycle must produce a before/after measurement, progress is only claimed when the number moves, and regressions are reverted rather than absorbed. The one-sentence contract:
No improvement may be claimed without a before/after row in a database, and the composite score may only move forward on evidence.
Everything else in this tutorial is scaffolding to make that sentence enforceable against an agent that is smart, fast, and — like all optimizers — inclined to satisfy the letter of its objective rather than the spirit.
Separate your loop into three layers with different mutation rules. Mixing them is the most common structural mistake: principles drift because they live in an editable procedure, or procedures ossify because they're tangled with laws.
Layer 1 — Constitution. The durable laws. Changes rarely, and only by explicit amendment. Crucially, every principle must be written as a checkable gate, not an aspiration. "Prefer quality" is a vibe; "no scorecard row without a deterministic check backing it" is a gate the wrap-up can pass or fail.
Layer 2 — Command. The session procedure: setup, cycle structure, wrap-up. Evolves freely — this is where self-amendment operates.
Layer 3 — State. The database and learnings log. Append-only, cross-session, and — critically — outside the rollback blast radius (§6). This is the loop's memory; if it lives in anyone's head or chat history, you don't have a loop, you have a series of unrelated sessions.
You cannot ratchet what you cannot compute. Define a composite score as a weighted sum of dimensions that matter for your system — for a voice platform that might be quality, completeness, humanness, readability, latency, and rebuild speed; for a data pipeline it might be freshness, correctness, cost, and time-to-add-a-source. The specific dimensions matter less than four properties:
Unmeasurable dimensions score zero. This is the single highest-leverage trick in the whole design. A dimension you can't measure yet isn't omitted — it scores 0, drags the composite down, and therefore becomes automatically the top-priority deficiency. The loop bootstraps its own instrumentation without you sequencing "build metrics first" by hand, because a missing metric is the lowest-hanging fruit by construction.
Normalize the baseline. Run everything measurable once, call that composite 100, and set a target (e.g., 120 = 20% relative improvement). Absolute numbers are meaningless; deltas against a frozen baseline are the whole game. When weights or judges change, old composites become incomparable — re-baseline explicitly rather than pretending continuity.
Include a "meta" dimension. The most valuable metric is usually not about the current system state but about the cost of change: how long and how many edits does it take to build a fresh unit of work (a flow, a pipeline, a service) from template to green? This "rebuildability" probe is what captures "the next thing we build starts from a higher floor" — the actual point of the exercise. Probe it every cycle with a disposable, namespaced artifact that is built, timed, and deleted, even in cycles whose fix was pure infrastructure.
Cap on violations. Hard invariant violations (safety rules, contract breaks) cap the composite regardless of other scores. Otherwise the loop learns that a fast, charming, rule-breaking system outscores a correct one.
Time-box the session, and within it, each cycle. A 30-minute session supports roughly one baseline cycle plus three or four improvement cycles; reserve the final few minutes for wrap-up unconditionally, because a loop that runs out of clock mid-cycle without reconciling is worse than a loop that did one less cycle.
Cycle 0 — Baseline. Read prior state (learnings, last scores), inventory what's measurable today, measure it, score zeros, persist, normalize.
Cycles 1..N:
Wrap-up (unconditional): reconcile external side effects (§6), verify zero disposable artifacts remain, print the scorecard trajectory, emit a constitution-compliance line per principle, write a learnings entry ending with the single recommended first action for the next session, and apply the merge gate (§6).
Many dimensions worth measuring (humanness of dialogue, readability of code) need an LLM judge. Two facts about judges, both learned expensively by everyone who builds these loops:
Judged scores are noisy. Identical code can score tens of points apart across runs. If judged scores gate the ratchet, your loop reverts good changes and merges bad ones on coin flips, and — worse — exhibits survivorship bias: you keep whichever runs happened to score high. Therefore: deterministic checks gate decisions; judged scores inform trends only. Pass rates, invariant counts, latency percentiles, field coverage — these move the ratchet. Judge scores are plotted, watched, and never trusted for a single-cycle verdict.
Judges are the softest attack surface for self-gaming. An agent that can edit its judge prompt will, eventually, "improve" scores by weakening the rubric — not maliciously, just because that's where the gradient points. So: freeze judge prompts at session start, persist their hash with every score (so cross-session comparability is provable), and put rubric changes behind the amendment process (§8). Anchor the rubric with fixed descriptions (0 = incoherent, 50 = functional but robotic, 80 = a competent human signs off, 100 = exemplary) so scores mean something across sessions.
The same principle generalizes beyond judges: claims are verified against independent sources, never trusted from the agent's own narration. "The UI works" is validated by a browser actually driving the page. "The docs are accurate" is validated by a claims registry where each documented statement has a machine check against the real system. "I cleaned up" is validated by querying the external provider's API (§6). The agent's log is a claim; the world is the evidence.
Two different physics apply, and conflating them is how loops cause damage.
Code rolls back. Run each session on its own branch from a tagged anchor, one commit per cycle, revert within the branch on regression, and apply a merge gate at wrap-up: the branch merges to main only if the final composite beat the session baseline; otherwise it's left intact for autopsy. This gives clean blast radius, no collision with human work on main, and — a nice bonus — makes autonomous scheduled runs trivial later (the merge gate becomes a pull request).
Two rollback subtleties that bite in practice: if your repo commits generated artifacts (compiled outputs derived from a source layer), the revert unit must be the source layer with all downstream layers recompiled in the same commit, or reverts silently un-revert when the next regeneration runs from unreverted upstream. And any concurrent writer to the same files — a refresh cron, another agent — must be paused at session setup and re-enabled at wrap-up, with the re-enable recoverable by the next session's sweeper if this one crashes. Add a cheap drift gate (recompile and diff at setup/CI) so divergence between layers can never accumulate silently.
The world does not roll back — it compensates. You cannot un-send an SMS, un-place a call, un-charge a card. For external side effects, the guarantee is not "undo" but: (a) every action is recorded before it happens, (b) every action has a defined compensating action, (c) a reconciler can prove the world returned to zero. The mechanism is a write-ahead side-effects ledger: no external action executes unless a ledger row exists first (intent, target, compensating action, status), the external ID is recorded after, and wrap-up diffs the ledger against the provider's API — zero orphans or the session fails loudly. The next session opens with a sweeper that re-reconciles prior sessions, so a crashed run can't leak resources silently.
Blast-radius fences live below the agent, in config the loop cannot amend: allowlists of targets it may ever touch (phone numbers, accounts, buckets), per-session budget caps where breach means abort-not-warn, and dedicated tagged resources (test numbers, test accounts) so reconciliation has an unambiguous ownership query. One semantic worth getting right: on a shared-with-the-world resource, unledgered outbound is a violation; stray inbound from strangers is log-and-ignore, or your reconciler becomes a false-alarm generator.
Loop state is exempt from rollback. A reverted cycle's ledger rows are exactly what cleanup needs; its scorecard row is exactly the evidence of what didn't work. The loop's schema is append-only: columns may be added, never dropped or truncated, and it lives on durable shared infrastructure — not a file next to the code it's rolling back.
The moment a measure becomes a target, optimizing the measure diverges from optimizing the goal. Ratchet loops are Goodhart machines by design, so you contain it structurally:
The complementary principle is shift-left: every defect found by an expensive check (a judged eval run, a live test, a production incident) must be converted into a cheap authoring-time gate — a compiler warning, a lint rule, a guard test — before the cycle counts as complete, and the commit must reference the new gate. This is how the system gets "harder to break with every iteration": the expensive lesson is paid once, then enforced for free forever.
"Self-improving" means different things at different levels of recursion:
Level 3 is where the loop becomes genuinely self-improving and where it becomes dangerous, and one rule contains it, borrowed from safety-config cascades: frozen invariants may only tighten, and only a human may merge changes to them. Guardrails, budget caps, allowlists, judge rubrics, the ratchet rule itself, teardown, the append-only schema — the loop may propose amendments to these, with evidence, into an amendments table; a human (or a human-reviewed adversarial pass) merges or rejects. Everything else in the procedure, the loop may amend itself after an adversarial review gate. Amendments land as diffs in commits, so the loop's evolution is itself auditable in git history.
On adversarial review generally: designs and amendments should pass through a different model or a differently-prompted reviewer before commitment. The author of a change is structurally the worst-positioned entity to find its flaws; a second set of weights repeatedly earns its keep catching schema forks, tautological checks, and drift the author's own verification missed.
Don't build all of this at once. The dependency order:
And before any of it: run a discovery phase against the real environment. Every assumption the design makes — artifacts are derived not committed, the database is reachable, the harness runs headless, no cron writes the same files — should be verified and reported before install, with an explicit instruction to halt on surprises rather than improvise. The two assumptions most likely to be wrong in any real system are "generated things aren't committed" and "nothing else writes what I write."
Every mechanism above exists because of a specific way loops die. The map:
| Failure mode | Countermeasure |
|---|---|
| Improvement claimed, nothing proven | No claim without a before/after DB row |
| Agent thrash across goals | One deficiency per cycle, written down first |
| Metrics never get built | Unmeasurable dimensions score 0 |
| Judge noise flips decisions | Deterministic checks gate; judges trend-only |
| Rubric softening / self-gaming | Frozen judges + hashes; tighten-only amendments |
| Goodhart on the eval set | Corpus/weights/rubrics frozen per session |
| Orphaned experiments everywhere | Mandatory verified teardown + wrap-up audit |
| Regression absorbed silently | Ratchet revert; violations revert mid-cycle |
| Bad session pollutes main | Session branch + composite merge gate |
| Revert undone by regeneration | Revert the source layer, recompile downstream atomically |
| Concurrent writer stomps session | Pause crons/writers at setup; sweeper re-enables |
| Un-undoable world damage | Write-ahead ledger, compensation, reconciliation vs provider API |
| Crashed session leaks resources | Next-session sweeper re-reconciles prior sessions |
| Runaway cost / wrong targets | Allowlists + hard budget caps below the agent's reach |
| Loop rewrites its own guardrails | Frozen invariants, human-merged, tighten-only |
| Author blind to own flaws | Adversarial review before commitment |
| Progress lives in chat history | Append-only state on durable shared DB, read at session start |
The smallest honest version — a starting point, not the destination:
SETUP: branch loop/<id>; tag anchor; pause concurrent writers;
sweep prior sessions; read learnings + last scores.
SCORE: composite = Σ wᵢ·dimᵢ ; unmeasured dim = 0 ;
violations cap composite ; baseline session-0 = 100.
CYCLE (≤ T minutes):
1 pick ONE deficiency, log it first
2 fix
3 rebuild probe: template→green, time, edits, delete, verify
4 evaluate vs FROZEN corpus (deterministic gates ratchet)
5 persist row (link independent evidence: run records, hashes)
6 composite ≥ prev ? commit "loop(N): <what> | a→b"
: revert, mark reverted, log why
WRAPUP (always): reconcile ledger vs provider API (zero orphans);
verify zero disposables; print trajectory + per-principle compliance;
propose amendments (frozen ⇒ human merge); learnings entry ends with
next session's first action; merge branch iff final > baseline.
A ratchet loop is not an agent that tries hard. It is an institution: a constitution that constrains, a procedure that executes, a database that remembers, gates that cannot be sweet-talked, and an amendment process that lets the whole thing evolve without ever being able to quietly lower its own standards. The system it improves should get harder to break and easier to rebuild with every iteration — and the proof should live in a database, not in anyone's memory.
Two installable Claude skills accompany this article:
The beginner edition. No database, no cloud accounts: git is the database, a CSV is the scorecard, and the score comes from things every repo has for free (tests passing, lint warnings). Read the whole skill in two minutes.
The full edition: Postgres scorecard, session branches with a merge gate, judged scoring done safely (judges inform, never decide), a write-ahead ledger for real-world side effects, and tighten-only self-amendment.
To install: in Claude.ai, open the .skill file and click Save skill; in
Claude Code, unzip into ~/.claude/skills/ (all projects) or a repo's
.claude/skills/ (one project). Then ask Claude to "improve my project
in a loop."