The word autonomy sometimes creates the wrong impression. We imagine an AI receiving a mission, disappearing into the terminal, and returning with a finished product. In practice, an autonomous loop looks less like autopilot and more like a control system.
It observes a state, performs an action, measures the gap from the goal, and decides whether to correct or stop. If the goal is vague, the measurement is poor. If there are no limits, the loop can continue long after it should have stopped.
The first guardrail: a success condition
“Improve the website” is not a success condition. There is no precise point at which the agent can say: this is finished.
A useful condition looks more like this:
Goal:
- the login flow works on mobile and desktop;
- authentication tests pass;
- lint exits 0;
- no file outside src/auth is modified.
The last line matters as much as the others. A loop can reach a technical result by changing too much. Scope is part of the result.
The second guardrail: independent verification
The producer and the verifier should be treated as different roles. Claude can write a fix and then run a test. But if the only criterion is “the code looks right”, the loop has no reliable measurement.
Possible validations include:
- a unit or integration test;
- a build with a zero exit code;
- lint or type-checking;
- a Playwright browser flow;
- a comparison with a reference screenshot;
- human review for product, security, or content decisions.
The proof should match the promise. A backend test does not prove that an interface is readable. A screenshot does not prove that Firestore permissions are correct.
The third guardrail: visible memory
A loop working across several turns needs persistent state. Not necessarily a database: a PROGRESS.md file may be enough.
# PROGRESS.md
## Goal
All contact form tests pass.
## Last cycle
- Cause identified: missing validation on the email field.
- File changed: src/contact/validate.ts
- Verification: npm test -- contact → OK
## Next step
Rerun the browser flow on mobile.
## Human decisions pending
None.
This keeps the conversation history from having to act as a project journal. It also lets another agent or a new session resume the work without inventing what happened.
The fourth guardrail: explicit limits
A loop needs a normal exit and an emergency exit:
- Turn limit: stop after 10 or 15 cycles.
- Time limit: do not let a task consume resources indefinitely.
- File limit: forbid rewrites outside the scope.
- Cost limit: monitor tokens, API calls, and external tools.
- Permission limit: ask before deployment, deletion, or sensitive data access.
These limits do not say that the agent will fail. They say that failure must remain recoverable.
Five ways a loop can go wrong
1. It repeats the same mistake
The test fails, Claude changes a file, the same test fails again, and another change is attempted without a new hypothesis. The loop appears active but is not making progress.
2. It optimizes the wrong metric
If the goal is “zero test errors”, the agent may be tempted to weaken the test or bypass the behaviour that made it fail. The constraint “existing tests must not be weakened” then becomes essential.
3. It silently expands the scope
A local fix becomes a complete refactor. The result may look cleaner, but the regression surface grows. For a loop, the smallest change is often preferable to the most elegant change.
4. It confuses no errors with success
A command can exit 0 while the feature remains unusable. This is why technical and visual validation should complement each other.
5. It crosses a human decision
An agent can choose default copy, pricing, business rules, or permissions. These are decisions, not bugs to be corrected automatically. The loop should flag them and stop.
The loop contract I would use
Before launching an autonomous task, I would write something like this:
Goal:
Feature X satisfies every acceptance criterion.
Context to read:
README.md, CLAUDE.md, docs/feature-x.md, and existing tests.
Allowed actions:
Modify only src/feature-x and its tests.
Required validation:
npm test -- feature-x
npm run lint
A browser flow through the three main states.
On failure:
Read the actual output, state a cause, make the smallest change,
then rerun only the relevant validations.
Memory:
Update PROGRESS.md after every cycle.
Stop:
Success, 12 cycles, or a decision requiring a human.
This text is short, but it includes what is most often missing from a prompt: the expected proof, the scope, the memory, and the stopping rule.
Autonomy as an architecture responsibility
Loop engineering is not about making AI as free as possible. It is about choosing exactly where it can bounce forward on its own and where it must hand control back.
For a lint correction, a fast loop is reasonable. For a database migration, payment, or user permission, you need more checkpoints, backups, and explicit human intervention.
That is the difference between “letting Claude work” and building a system that knows what working correctly means.
The verification and boundary principles described here map directly to Claude Code goals and hooks. Commands change with versions; the loop contract remains useful.