DEV Community

Ali Suleyman TOPUZ
Ali Suleyman TOPUZ

Posted on Originally published at topuzas.Medium on

Five Rules for a Claude Code Agent That Runs Itself

I have a specific memory that made me write this article. It was a Tuesday night, I had a Claude Code session running a migration script across forty-something files, and I went to make coffee. I came back twelve minutes later to a session that had declared itself “done,” committed the changes, and moved on to writing documentation for a feature that did not exist yet. Eleven of the forty files were untouched. It had gotten confident, or whatever the right word is for a model that stops checking its own work, and it just kept going as if nothing had gone wrong.

That was not the first time. I had tried swapping models, thinking a smarter model would just know when it was actually finished. It did not help much. I tried longer, more emphatic prompts, the kind where you write “IMPORTANT: make sure you actually finish” in caps and hope the emphasis lands. That did not help either. What actually fixed it was not the model at all. It was the harness around the model, the scaffolding of files, checkpoints, and independent verification that decides what the agent is allowed to believe about its own progress.

That is the core thesis of this piece, and I want to say it plainly before the five rules, because it is easy to read a list like this and think it is about prompting technique. It is not. A Claude Code agent that runs for an hour, a day, or across a dozen sessions is not going to be reliable because you found the right sentence for the top of the prompt. It is going to be reliable because you built a system around it that does not depend on the model’s self-report, the same way you would not run payroll based on whether the intern who ran it “felt good about the numbers.”

I read a piece on Simplifying AI about building a Claude Code agent that could run unattended, and it got the framing right: models are not the bottleneck for long-running autonomous work anymore, the surrounding discipline is. Where I wanted more was the specifics, actual file layouts, actual stop conditions, actual code for the checkpoint and review mechanics. So here are the five rules I actually use now, with working examples, after enough burned evenings to know which ones matter.

Rule 1: Write a controllable, evidence-based definition of done

This is the rule that fixes the most damage for the least effort, and it belongs in CLAUDE.md because that is the file Claude Code loads into context automatically at the start of every session, no reminder needed.

The failure mode is almost always the same shape: you tell the agent to finish a task, and somewhere in its own reasoning it decides it is finished based on a feeling rather than a fact. “I’ve addressed the main issues” is a feeling. “35 of 50 items on the checklist are done, and I believe the rest are lower priority” is a feeling wearing a percentage sign. Neither one is evidence.

Here is the bad version, the kind of definition of done I used to write without thinking about it:

Stop when you are confident the migration is complete and the tests
would probably pass.
Enter fullscreen mode Exit fullscreen mode

That sentence gives the model an out on every axis that matters. “Confident” is not measurable. “Would probably pass” means it never actually ran them. I have watched an agent read a line almost exactly like this and conclude, in its own words, that it was “reasonably confident” after modifying nine files out of forty-one, because nine felt like meaningful progress and the prompt never told it what number it needed to hit.

Here is the version I use now:

Stop only when ALL of the following are true, and paste the evidence
for each one before declaring the task complete:
1. `npm test` exits with code 0. Paste the full exit code and the
   final summary line, not a paraphrase.
2. `git diff --stat` shows every file listed in PLAN.md as modified.
   Paste the output.
3. `npm run lint` exits with code 0. Paste the output.
4. Every checklist item in PROGRESS.md is marked [x], not [] or [~].
If any of these is false, you are not done. Say so explicitly and
continue working. Do not summarize what you "mostly" completed.
Enter fullscreen mode Exit fullscreen mode

The difference is not politeness or emphasis, it is falsifiability. A definition of done that a model can satisfy by describing its feelings is worthless no matter how many exclamation points you put around it. A definition of done that requires pasting a real exit code closes off the easiest way an agent cheats itself, which is rounding “almost passing” up to “passing.”

Addy Osmani makes basically this same point in his writeup on loop engineering, using Lighthouse scores and test suites as examples of deterministic stop criteria instead of vague goals like “make the UI good.” One thing I’d add from months of running these loops: it is not enough to define what “done” looks like, you also need to define what “stuck” looks like, or an agent that cannot reach done will loop forever burning tokens. The stop signals I put in CLAUDE.md alongside the definition of done:

+---------------------------------------------+------------------------------------------+
| Signal | What to do |
+---------------------------------------------+------------------------------------------+
| Same failing command run 3 times with no | Stop. Write the failure to DECISIONS.md, |
| change in the error output | do not attempt a 4th time |
+---------------------------------------------+------------------------------------------+
| Two consecutive turns with no measurable | Stop. The approach in PLAN.md is probably |
| progress against the checklist | wrong, not the execution of it |
+---------------------------------------------+------------------------------------------+
| A file you were told not to touch got | Stop immediately, do not self-correct, |
| modified | flag it in PROGRESS.md for a human |
+---------------------------------------------+------------------------------------------+
| Turn count exceeds the budget set at the | Stop and hand off, do not ask for "just |
| start of the session | five more turns" |
+---------------------------------------------+------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

Anthropic’s own writeup on long-running Claude sessions for scientific computing describes something similar under what they call the Ralph loop: an orchestration layer that, when the agent claims completion, kicks it back into context and asks whether it is really done, iterating until it gets an honest signal instead of trusting the first “I’m done” at face value. Same principle as rule one, automated one level up. You are not trusting the claim, you are demanding the evidence.

Rule 2 and Rule 3: build a memory structure that survives a crash

Rules two and three are one idea split in half. Rule two gives the agent a memory that cannot drift. Rule three gives it a memory that cannot be lost. Together, if your laptop dies, your session times out, or you run out of context mid-task, the next session picks up exactly where the last one left off instead of re-deriving the plan from scratch, or worse, re-deriving it wrong.

The file layout I use now, in the root of every project where an agent is going to run unattended for more than one session, looks like this:

project-root/
  CLAUDE.md <- loaded automatically every session, has the rules
  SPEC.md <- the contract, agent NEVER edits this file
  PLAN.md <- current approach to hitting the spec, editable
  PROGRESS.md <- read FIRST every new session, updated constantly
  DECISIONS.md <- append-only log, never rewritten, only appended
Enter fullscreen mode Exit fullscreen mode

SPEC.md is the one file the agent is explicitly forbidden from editing , and I say so directly in CLAUDE.md: "You may read SPEC.md but you may never modify it. If you believe the spec is wrong, write your objection to DECISIONS.md and stop for human review." A long-running agent under pressure to finish will, if given the option, quietly loosen the requirements rather than admit it cannot meet them. I have seen an agent narrow "supports concurrent writes from up to 50 clients" down to "supports concurrent writes" in its own summary, not maliciously, just because the smaller claim was easier to satisfy. If the spec is a file it cannot touch, that drift has nowhere to hide:

# SPEC.md
## Goal
Migrate the `orders` table from the legacy `status` enum (5 values) to
the new `status_v2` enum (9 values) without downtime.
## Hard requirements
- Zero rows may end up with a NULL status after migration.
- The migration must be reversible via a single down-migration file.
- Existing API consumers reading `status` must continue to work
  unchanged until the deprecation date (see below).
## Out of scope
- Do not touch the `orders_history` table.
- Do not change API response shapes in this task.
Enter fullscreen mode Exit fullscreen mode

PLAN.md is where the agent's current approach lives, and unlike SPEC.md it is meant to be rewritten as understanding improves. I keep it short and checklist-shaped so PROGRESS.md can reference it item by item:

# PLAN.md
1. [] Add status_v2 column, nullable, with a default backfill job
2. [] Write dual-write logic so both columns update together
3. [] Backfill existing rows in batches of 5000
4. [] Add a read-shim so old API consumers still see `status`
5. [] Verify zero NULL rows in status_v2
6. [] Cut over reads to status_v2 behind a feature flag
Enter fullscreen mode Exit fullscreen mode

PROGRESS.md is the file every new session reads first, before touching any code, and the one that gets rewritten most often during a run. This is the closest analogue to what Anthropic's research team called the agent's "portable long-term memory" in their scientific computing work, where the progress file tracked completed work, failed approaches with the reason they failed, and known limitations, so a new session would not waste an hour rediscovering a dead end the last one already found:

# PROGRESS.md
Last updated: session 4, 2026-08-21 14:02 UTC
## Status
On step 3 of PLAN.md (backfill). 214,000 / 1,340,000 rows backfilled.
## What's done
- [x] Step 1: status_v2 column added, migration 0042 applied
- [x] Step 2: dual-write logic in OrderService, covered by 6 new tests
## What failed, and why (do not retry these)
- Tried backfilling in batches of 50,000: caused replication lag
  alerts on the read replica. Dropped to batches of 5,000.
- Tried a raw SQL UPDATE for the backfill: hit a lock timeout on the
  orders table during business hours. Switched to an app-level job.
## Next step
Resume the batch backfill job at row offset 214,000. Do not restart
from 0, the job is idempotent per-row but slow to re-scan.
Enter fullscreen mode Exit fullscreen mode

DECISIONS.md is append-only, literally, the instruction in CLAUDE.md is "never delete or rewrite a line, only add new ones with a timestamp." It is the record of why, not what, and the file that keeps a five-session project from contradicting itself in session six:

# DECISIONS.md
2026-08-19 10:14 - Chose batch size 5000 over 50000 after replication
lag alerts. See PROGRESS.md session 2 for the incident detail.
2026-08-20 16:40 - SPEC.md requires zero-downtime, so the read-shim
stays in place even after backfill completes, until the deprecation
date named in SPEC.md. Do not remove it early to "clean up."
2026-08-21 09:02 - Objection: SPEC.md says "50 concurrent clients"
but load testing only validated to 30. Flagging for human review,
not proceeding past this without an answer.
Enter fullscreen mode Exit fullscreen mode

That last entry is the pattern from rule one again: the agent hit a limit, did not quietly redefine the spec to make it true, and left a paper trail instead. None of these four files is clever on its own. Together they move the agent’s memory out of the conversation history, where it degrades under compaction, and into small, structured, version-controlled files a fresh session can read in a second and trust completely.

Rule 4: checkpoint and resume for anything that spans hours or days

Rules two and three handle memory across sessions you control. Rule four is about surviving the sessions you do not, the crash, the timeout, the context window that fills up mid-task with no warning. Google’s engineering guidance on long-running agents, from their work on the Agent Development Kit, frames this as a state machine problem rather than a conversation problem: give the agent an explicit set of named steps and persist which one it is on, so the next process to pick up the work knows its exact position without guessing from chat history.

For a task that runs for hours, the state machine might look as simple as this:

START -> SCHEMA_MIGRATED -> DUAL_WRITE_ENABLED -> BACKFILL_RUNNING
  -> BACKFILL_VERIFIED -> READ_SHIM_REMOVED -> COMPLETE
Enter fullscreen mode Exit fullscreen mode

The Google pattern persists this to a database (SQLite locally, Cloud SQL in production) and updates it atomically through the tool layer, so a crash mid-task means the next run rehydrates the exact state and resumes rather than starting over or, worse, guessing which step it was on from ambiguous log output. You do not need their full infrastructure to get the same benefit inside a Claude Code project. A flat JSON checkpoint file, committed alongside PROGRESS.md, does the same job at a much smaller scale:

# checkpoint.py
import json
import os
from datetime import datetime, timezone
CHECKPOINT_PATH = "checkpoint.json"
VALID_STATES = [
    "START", "SCHEMA_MIGRATED", "DUAL_WRITE_ENABLED",
    "BACKFILL_RUNNING", "BACKFILL_VERIFIED",
    "READ_SHIM_REMOVED", "COMPLETE",
]
def write_checkpoint(state: str, detail: dict):
    if state not in VALID_STATES:
        raise ValueError(f"unknown state: {state}")
    payload = {
        "state": state,
        "detail": detail,
        "updated_at": datetime.now(timezone.utc).isoformat(),
    }
    tmp_path = CHECKPOINT_PATH + ".tmp"
    with open(tmp_path, "w") as f:
        json.dump(payload, f, indent=2)
    os.replace(tmp_path, CHECKPOINT_PATH) # atomic on POSIX
def resume():
    if not os.path.exists(CHECKPOINT_PATH):
        return {"state": "START", "detail": {}}
    with open(CHECKPOINT_PATH) as f:
        return json.load(f)
if __name__ == " __main__":
    current = resume()
    print(f"Resuming from state: {current['state']}")
    print(f"Detail: {current['detail']}")
Enter fullscreen mode Exit fullscreen mode

Writing to a temp file and using os.replace for the swap is the detail I would not skip, it stops you from ending up with a half-written, corrupt checkpoint if the process dies mid-write, exactly the moment you most need it intact. In CLAUDE.md I tell the agent explicitly: "Before starting any step, call resume() and confirm the current state. After completing any step, call write_checkpoint() before moving to the next one, not after." That ordering matters more than it looks, checkpointing after the fact means a crash between finishing the work and recording it loses the record even though the work is done, and you get silent duplicate work on resume.

If you want queryable history instead of a single overwritten file, swap CHECKPOINT_PATH for a one-line SQLite insert, id, state, detail, updated_at, via the sqlite3 module already in Python's standard library. No hosted database, no extra dependency, and you get every past checkpoint instead of just the latest one.

Either version gets you the property that matters: a task that runs across hours or days survives being interrupted, because “where was I” is a read from a file, not a question the model reconstructs from its own memory of a conversation that may already be compacted away.

Rule 5: the agent must never grade its own work

This is the rule I resisted longest, because it feels redundant when the agent already ran its own tests and told you they passed. It is not redundant. An implementing session has every incentive, structural, not moral, to interpret ambiguous results charitably, because it is the same context that has been staring at the problem for an hour and wants to be done. That is not a flaw you can prompt away, it is a property of how these sessions build momentum toward “finished.”

The fix is procedural, not another sentence in the prompt: verification runs in a fresh /clear session, one with no memory of writing the code, and that session's job is to actually execute the test suite and report the exit code, not read the implementing session's summary and nod along. Addy Osmani's writeup on loop engineering describes this as a two-role pattern, one sub-agent drafts, a separate one verifies, because a single agent checking its own work tends to have blind spots on exactly the dimensions it was already weak on.

In practice, my session boundary looks like this. The implementing session works normally, updates PROGRESS.md, and when it believes it has met the definition of done from rule one, it writes a short note: “Ready for verification. Claimed state: COMPLETE.” Then I run /clear, and a fresh session gets a prompt like this:

You are the verification session. You did not write this code.
Do not trust anything in PROGRESS.md about test results, re-derive
them yourself.
1. Read SPEC.md and PLAN.md to understand what "done" means here.
2. Run the full test suite yourself: `npm test`. Paste the real
   output, not a summary.
3. Run `git diff --stat` against the base branch and check every
   changed file against SPEC.md's out-of-scope list.
4. If anything fails, write the failure to DECISIONS.md with the
   exact error, and set PROGRESS.md status back to IN_PROGRESS.
5. Only if everything genuinely passes, mark PROGRESS.md as VERIFIED.
Enter fullscreen mode Exit fullscreen mode

The reason a fresh session matters, and not just a differently-worded prompt in the same session, is that context is sticky. A session that has spent forty tool calls converging on a solution has built up a prior that the solution is probably right, and that prior leaks into how it reads ambiguous test output. “3 tests failed, but they look flaky” gets read generously by the session that wrote the code and skeptically by a session with no stake in the outcome. /clear is not a formality, it is the mechanism that removes the bias.

This is also where rule one and rule five reinforce each other. A vague definition of done gives the verification session nothing concrete to check. A definition of done built on exit codes and file diffs gives it an actual job: run the command, read the number, compare it to the requirement. No interpretation required, which is exactly the point.

The copy-paste template

Here is the minimal version of all five rules, ready to drop into a new project. Start with PROGRESS.md:

# PROGRESS.md
Last updated: <session number>, <UTC timestamp>
## Status
<IN_PROGRESS | READY_FOR_VERIFICATION | VERIFIED | BLOCKED>
## What's done
- []
## What failed, and why (do not retry these)
-
## Next step
Enter fullscreen mode Exit fullscreen mode

And a verification script skeleton you can adapt to whatever your test runner actually is:

#!/bin/bash
# verify.sh - run this in a fresh /clear session, never in the
# session that implemented the change.
set -e
echo "== Running test suite =="
npm test
TEST_EXIT=$?
echo "== Running lint =="
npm run lint
LINT_EXIT=$?
echo "== Checking diff against out-of-scope files =="
git diff --stat main -- . ':!node_modules'
if [$TEST_EXIT -eq 0] && [$LINT_EXIT -eq 0]; then
  echo "PASS: exit codes clean, evidence above. Human should still spot-check the diff."
  exit 0
else
  echo "FAIL: do not mark PROGRESS.md as VERIFIED. See exit codes above."
  exit 1
fi
Enter fullscreen mode Exit fullscreen mode

None of these five rules is complicated on its own, and that is the point. What stopped my agent from quietly declaring victory with eleven files unmigrated was never a smarter model. It was a definition of done that could not be satisfied by a feeling, a memory structure that survived a crash, a checkpoint that knew exactly where the work stood, and a verification session with no reason to be generous. Fix the harness, not the model, and the model stops needing rescuing.

Tags: claude-code, ai-agents, software-engineering, developer-tools, prompt-engineering, devops, autonomous-agents

Top comments (0)