DEV Community

Cover image for Three weeks after I said CLAUDE.md writes itself, it added 4 more rules without me
Michel Faure
Michel Faure

Posted on • Originally published at dev.to

Three weeks after I said CLAUDE.md writes itself, it added 4 more rules without me

A thesis, three weeks later

On April 28th, I published an article on DEV.to that made four claims about a CLAUDE.md file — the one that constrains the coding agent at each session — and ended with this sentence: "the CLAUDE.md is never finished, and that's precisely why it works" (4 incidents, 4 rules: how my CLAUDE.md wrote itself). That was a thesis, not a metaphor. Three weeks passed. The file added four rules without me.

What I mean is that I didn't write them on a day I sat down to write rules. I received them on the days an incident had produced them, and all I had to do was record them before they evaporated in the flow of the project. The difference, on paper, seems thin. In the practice of a solo dev piloting an agent in production, it's doctrinal.

One clarification before the list: this article's title almost said "five rules." live-snapshot-cache.md was committed on April 25th, three days before the pivot article was published. It doesn't count. I'd rather have the honest number than the comfortable rounding.

The audit, measured by git

No narrative without raw material. Here is what git log --diff-filter=A --follow on .claude/rules/ returns between April 28th 2026 (publication of the pivot article) and May 21st 2026 (today) — four new files strictly post-publication.

cache-auth-contract.md — committed May 2nd. Born from a technical debt audit, not a production crash. It's a Friday late afternoon. Niran is settled two desks away, headphones on, a closed burger box in the corner. I'm going through docs/dette/AUDIT-2026-04-30.md section D-20 on the right screen, code on the left. Reading through getCachedFormateurs, I understand that the unstable_cache is shared across all users — session not propagable. If someone exposes this function via an API route without a guard, it's a silent RBAC leak. I look up to talk to Niran about it. He removes his headphones, listens, says "Yeah, that bites." He puts them back on. The rule gets written that evening.

// .claude/rules/cache-auth-contract.md — anti-pattern to prohibit

// Flaw: no guard
export async function GET() {
  const formateurs = await getCachedFormateurs()
  return Response.json(formateurs)
}

// Correct
export async function GET(req: NextRequest) {
  const supabase = await createSupabaseServer()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return new Response('Unauthorized', { status: 401 })
  const profile = await getUserProfile(user)
  if (!canAccess(profile, 'communication')) return new Response('Forbidden', { status: 403 })
  const formateurs = await getCachedFormateurs()
  return Response.json(formateurs)
}
Enter fullscreen mode Exit fullscreen mode

inscrit-nom-prenom-required.md — committed May 14th. "Hm, it's buggy." — Catherine, two hours earlier. "But it's a quick fix." The daily drift probe sonde_contacts_orphelins_inscrits surfaced an inscrit-status contact with an empty first name — a child named Loubna, imported from Airtable where the first name lived in a separate unmapped column. The grep that followed found sixteen similar cases. What would have broken regular attendance tracking (Cannot read properties of undefined) gets caught by a Postgres CHECK constraint that closes the incident class at the root.

-- .claude/rules/inscrit-nom-prenom-required.md
CHECK (
  statut <> 'inscrit'
  OR (
    nom IS NOT NULL AND nom <> ''
    AND prenom IS NOT NULL AND prenom <> ''
  )
)
Enter fullscreen mode Exit fullscreen mode

Without this CHECK, the rule stays textual in CLAUDE.md and the next import brings back a seventeenth case before the next probe. With it, the INSERT fails, and the import surfaces the problem at the source.

contrat-formation.md — committed May 16th, in the wake of ADR-0068. It's the longest rule, because the professional training contract is a Snapshot where every column carries its guarantee of immutability. motivation_code, text_version, cases_cochees, pdf_storage_path — frozen at generation, never recalculated retroactively. An evolution of the contract is never a rewrite of the Snapshot, it's a new event with a new text_version. The rule exists because the three-year Qualiopi audit rests entirely on the immutability of the generated PDF and the associated trainee signature — a retroactive recalculation would be enough to make the file indefensible.

hybrid-snapshot-live-reset.md — committed May 19th, two days before this article. Before sending the fifty-three Phase 2 re-enrollment SMS messages, a pre-flight audit surfaced that one token out of the fifty-three was consumed — created in test mode that morning, clicked, used_at non-null. If the Phase 2 SMS went out as-is, the link /r/<short_code> would return a 410 Gone, the contact loses their conversion, the support ticket lands at end of day. The generateTokenForContact helper was resurrecting the object (frozen identity Snapshot) but forgetting to reset the Live usage marker. Fix commit 07ed02d. The rule names the pattern, sets it against R6 Live / Snapshot / Cache of the toolkit, of which it is the project-specific extension.

The rule that couldn't have been written before

Let's go back to the second rule, Catherine's and Loubna's. Sure, I could have, on March 21st 2026 the day the first CLAUDE.md was created, abstractly written "an enrolled contact must have a first and last name". But that rule wouldn't have held, because it would have been read, nodded at, and would never have produced a Postgres CHECK constraint. The rule that holds isn't the moral statement, it's the material mechanism — the audit SQL that must always return zero, the migration that closes the incident class at the root.

-- DB coherence audit — must always return 0
SELECT COUNT(*) FROM contacts
WHERE statut = 'inscrit'
  AND ((nom IS NULL OR nom = '') OR (prenom IS NULL OR prenom = ''));
Enter fullscreen mode Exit fullscreen mode

To produce that SQL, you needed the incident. To write the Why paragraph of the rule (which names the sixteen patched contacts, the Airtable origin, the probe that surfaced case-zero), you had to have lived through the widening. No "I write my doctrine on day one" produces that level of specificity. The rule isn't a drafted precept, it's a hardened scar.

Four observations about sedimentation

Four things become visible when you line up the four rules and look at them from a distance.

First, none of the four could have been written before their incident. Not because I lacked imagination on March 21st, but because the material precision of a useful rule comes from the encounter with a concrete case. A CHECK constraint, a tunnel mapping to DREETS paper boxes, a used_at reset in the same transaction as the SELECT — details that operate, that abstraction would never have produced.

Second, all of them cite a commit, a migration, a session log, or an ADR. No floating rules. I learned this traceability in the pivot article, but I hadn't measured it as a mechanism. Today I do: if the rule doesn't carry its material anchor, it doesn't do its job. The agent can read it, the human reader too, and both can trace back to the incident if needed.

Third, three out of four prohibit an anti-pattern, the fourth freezes a Snapshot. The negative rule dominates, exactly as #20 predicted. The abstract positive rule (use Server Components by default) gets read and forgotten. The anchored negative rule (a getOr* helper that returns a Snapshot without resetting Live markers silently introduces a dead link at the next reuse) gets read and remembered because it carries its consequence.

Fourth, and this is the observation that changes the status of the CLAUDE.md, I didn't invent these rules, I received them. The distinction sounds rhetorical, it isn't. Inventing a rule assumes you imagine it then write it. Receiving a rule assumes an incident produced it and you record it before it evaporates. In the first regime, the file is a solitary writing act that claims exhaustiveness. In the second, the file is a sedimentation device that demands maintaining a holding space — an open notebook, a regular review session, a git log grep at short intervals. The work is no longer to write, it's to catch.

Why it works now

Whatever we think about documentation best practices, a CLAUDE.md written in one sitting at project start doesn't hold. It ages in two weeks, it accumulates rules that nobody invokes, and the agent ends up reading it mechanically without loading it into working memory. The CLAUDE.md that holds doesn't age — it sediments. Each incident deposits its layer, the file carries the material memory of the project rather than its imagined documentation.

Three weeks of post-#20 practice materially confirm the pivot article's thesis. But they add a nuance that #20 hadn't formulated: for the file to sediment, there must be something that solicits it. A daily drift probe, a monthly debt audit, a sending pre-flight that asks "are all fifty-three tokens actually active?" — these are the devices that produce the incidents that produce the rules. Without them, the file stays on its naive day-one version, and the project drifts in silence.

Coda

A CLAUDE.md that no longer writes itself is a file that no longer works — either because the project is dead, or because the devices that solicit the incident have disappeared. Three weeks after publishing the thesis, I can verify it on raw material: four rules, four incidents, four Why paragraphs that couldn't have been written before. The file kept writing itself while I was doing something else. But I also know now what keeps it alive — and what would be enough to kill it if I stopped paying attention.

If you maintain a CLAUDE.md on a project where you're piloting an agent in production, ask yourself the material question: what has it added without you in the last three weeks? If the answer is nothing, it's probably not the doctrine that's run dry. It's the device that produces incidents that's gone dark.


Sequel to 4 incidents, 4 rules: how my CLAUDE.md wrote itself (April 28th, 2026). Measurements at 23 days' distance, verified on .claude/rules/ of the Rembrandt repo — 18 rule files currently active, 4 strictly post-pivot. No Counterpart Toolkit in this sequel — that topic lives in a separate series.

Top comments (0)