Evolving Claude Md
ASecuritySet up CLAUDE.md to maintain a living Decisions & Learnings log that stays bounded as the project evolves — a format contract enforced at write time, four pruning pressures, and a coverage check for the essentials. Use when the user says "make CLAUDE.md evolve", "self-updating CLAUDE.md", "add a learning mechanism to CLAUDE.md", "decisions log", "ADR-style notes", "how do we keep CLAUDE.md current", "compact CLAUDE.md", "CLAUDE.md is getting too big" — or when a CLAUDE.md exists with no mecha...
- 9 stars
- 0 votes
- 0 copies
- 2 views
- Added September 5, 2026
Works with
Security analysis
100/100Pro scans all 8 files and shows the line behind each finding
npx -y skills add alexmond/alexmskills --skill evolving-claude-md --agent claude-codeAre you the author of Evolving Claude Md?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/alexmond-evolving-claude-md)---
name: evolving-claude-md
description: Set up CLAUDE.md to maintain a living Decisions & Learnings log that stays bounded as the project evolves — a format contract enforced at write time, four pruning pressures, and a coverage check for the essentials. Use when the user says "make CLAUDE.md evolve", "self-updating CLAUDE.md", "add a learning mechanism to CLAUDE.md", "decisions log", "ADR-style notes", "how do we keep CLAUDE.md current", "compact CLAUDE.md", "CLAUDE.md is getting too big" — or when a CLAUDE.md exists with no mechanism for keeping itself current. Complements (does not replace) the built-in `init` skill, which bootstraps the description of the codebase — this adds the mechanism that keeps it from bloating.
---
**Codex:** Read [the client path](references/clients.md) before applying this workflow.
# Evolving CLAUDE.md
> **Try it:** `/evolving-claude-md:evolving-claude-md` — or say "make CLAUDE.md evolve".
CLAUDE.md is read into Claude's context on every turn. Every byte costs tokens. So the goal isn't "log everything we learned" — it's **a small, well-pruned set of durable decisions that future-Claude needs, with everything else linked to or archived**.
This skill wires that. Three mechanisms keep it healthy automatically:
| Hook | When | What it does |
|---|---|---|
| **SessionStart** | start of every session | `audit-claude-md.py` — if the D&L section is bloated, injects a recommendation to compact |
| **PreToolUse** on `Write\|Edit` of `CLAUDE.md` | before each edit | `lint-claude-md.py` — rejects new entries that violate the format (no topic tag, no date, body over 500 chars) |
| **PostCompact** | after context compaction | re-runs the audit so the assistant sees the current D&L state without paying for the full file twice |
When installed as a **plugin**, the three hooks ship inside the plugin (`hooks/hooks.json`, pathed via `${CLAUDE_PLUGIN_ROOT}`) and register automatically once enabled — nothing to add to your settings. For a **manual install**, copy the three scripts to `.claude/skills/evolving-claude-md/` and add the hooks to `.claude/settings.json` (see *Setup checklist*). Disable individually by removing the entry; disable all via `disableAllHooks: true` in settings.
> Not for documenting a codebase from scratch — that's `init`. This adds the
> mechanism that keeps the file current and bounded once it exists.
## Format — the contract every entry must follow
```
- YYYY-MM-DD — **topic-tag** — short statement. Why: brief reason. [Optional: see → docs/decisions/...].
```
- **YYYY-MM-DD** — calendar date, no relative dates.
- **`**topic-tag**`** — kebab-case, one or two words, MANDATORY. Reuse existing tags where they fit; the lint hook surfaces the inventory. Pick a stable vocabulary per project (e.g. `auth`, `build`, `schema`, `ci`, `perf`).
- **One sentence of *what***. The *why* is the load-bearing half — lead with constraint, incident, or preference.
- **Aim for 200 chars in the body, max 3 lines; the lint hook rejects anything over 500.** The gap between the two is deliberate: 200 is the target that keeps the log scannable, 500 is where an entry is provably a design doc. Past either, move the detail to `docs/decisions/{YYYY-MM-DD}-{topic}.md` and keep a one-line teaser linking there.
Examples:
```
- 2026-06-10 — **build** — switched from system `mvn` to the checked-in `./mvnw`. Why: CI and dev were on different Maven versions; wrapper pins it.
- 2026-06-09 — **auth** — env API key now ignored in favour of subscription login (forceLoginMethod). Why: key value rotates; setup must survive it.
- 2026-06-08 — **schema** — `verified` flag added inline on the record. Why: downstream filter needs a trusted-only view. See → docs/decisions/2026-06-08-schema.md.
```
## What to log — the bar decides, the triggers only nominate
**The bar. Write the candidate as a sentence that is true about this repo
*tomorrow*, then name what a future session does differently knowing it.** If
the only true sentence is "we did X", there is no entry — git log, the tickets
and the changelog already hold that, and a log that repeats them stops being
read. Most sessions produce zero entries, and zero is the right answer to a
session that merely shipped.
A delivery earns an entry only when it **taught** something a reader can't see
in the code: a constraint, a trap, a reversal, a rule. "Shipped 0.4.0 with the
new estimator" is a changelog line. "The publish plugin ignores `deploy.skip`,
so skipped modules still publish" is an entry.
These six *nominate* — the bar above decides:
1. A non-trivial architectural decision (stack, schema, tradeoff resolved).
2. Durable user feedback (preferences, things to never do, validated approaches).
3. A non-obvious gotcha (build quirks, library traps, third-party API limits).
4. A convention established or revised.
5. A scope shift (something moved in/out, priorities reordered).
6. An external dependency or service added / replaced / removed.
## What NOT to log
- **A delivery.** Shipping, bumping, adding a feature — the release is not the
learning. Measured on one repo: 63% of entries restated a version its own
`CHANGELOG.md` already documented, which is what the audit's CHANGELOG-mirror
check now reports.
- **Anything a durable artifact already carries.** If the repo keeps a
changelog, ADRs or tickets, the entry is earned only by the part that isn't
in them.
- Routine code changes ("renamed X to Y") — git log has it.
- Transient task state — the task list has it.
- Anything obvious from reading the code now.
- Duplicates of an existing convention/gotcha — update the existing entry instead.
- Mega-context dumps. If you find yourself writing >200 chars, you're writing a design doc; put the doc in `docs/decisions/` and link to it.
## Superseding an entry, not just striking it
A reversal used to be recorded by striking the old entry — which works only if
whoever writes the new one remembers. Name the entry you are replacing and the
link becomes checkable:
```
- 2026-09-16 — **build** — switched to `./mvnw`. Why: CI drifted. Supersedes: 2026-01-01 build
```
The audit then reports any entry whose target is **still unstruck** (so a dead
rule is still being read as current) or whose target **does not exist** (a
typo'd date or tag). The link itself is exempt from the body cap — charging
bookkeeping against the prose budget would just price it out of use.
## Load evidence — what actually loaded, not what should have
`record-loads.py` runs on `InstructionsLoaded` and appends one line per event to
`.claude/evolving-claude-md/loads.jsonl`: which instruction file loaded, in which
session, with the `load_reason`. That makes two otherwise invisible failures
checkable — a `.claude/rules/*.md` whose globs never match anything read (dead,
though its content is fine) and a `CLAUDE.md` that never loads at all (skipped
over 4 MiB, or in a path the client doesn't read). Both wait for
`load_min_sessions` (default 5) of evidence. Detail:
[references/audit-checks.md](references/audit-checks.md).
## Recent / Historic split
The D&L section is split into two subsections:
```
### Decisions & Learnings (Recent — last 14 days)
- 2026-06-10 — **topic** — ...
- 2026-06-09 — **topic** — ...
### Historic (older than 14 days · see git log for the build-up)
- 2026-05-XX — **topic** — ... [or one-line teasers pointing at archived files]
- 2026-Q1 — 14 entries archived → docs/decisions/2026-Q1.md
```
The Recent section is what Claude actively scans every turn. Historic stays minimal — one-liners with teasers. New entries always go into Recent.
## Pruning, graduation, archiving — four downward pressures
### 1. Strike-through on reversal
When a decision is reversed, strike-through with `~~...~~` and add a follow-up explaining the change. Don't silently delete.
### 2. Merge same-session clusters (the pre-14-days lever)
When one session lands 4+ entries about one piece of work, collapse them into a
single entry with a broader tag, moving per-aspect detail to
`docs/decisions/{date}-{topic}.md`. This is the *only* compaction that works
before the 14-day mark — graduation needs stability, archiving needs age. When
the audit says "Compaction RECOMMENDED" but every entry is young, merge is what
is left. Triggers and the reversibility rule:
[references/audit-checks.md](references/audit-checks.md).
### 3. Graduation — when a pattern stabilizes
When the same `**topic-tag**` appears in 3+ entries AND the latest is ≥14 days old without a contradiction, the pattern is stable. **Graduate** it: rewrite as a one-line rule in **Conventions** (or **Gotchas** if it's a trap), strike through the D&L entries, leave a single graduation line `- YYYY-MM-DD — **topic** — graduated → see Conventions § X`.
The audit hook surfaces graduation candidates automatically. The skill's job is to act on the surfaced suggestion when the user OKs it.
### 4. Quarterly archive
Run `archive-decisions.py --cutoff YYYY-MM-DD --apply` at the end of each quarter. The script:
- Moves all entries older than the cutoff to `docs/decisions/{YYYY-Q}.md`
- Replaces them in CLAUDE.md with a single teaser line
- Preserves causality (strike-throughs, graduation links) in the archive
CLAUDE.md never grows monotonically — quarter ends, entries move out.
## `/evolving-claude-md:compact` — the pressures as one reviewed edit
Runs the pressures in order — graduation **before** the archive, so a lesson
that should be a standing rule never lands in the quarterly file — drafting
every edit for approval (`plan` / `yes`). Data: `compact-claude-md.py`
(read-only). The full procedure lives in the command itself.
## Nested CLAUDE.md
A monorepo can carry `packages/api/CLAUDE.md` beside the root file, and Claude
Code loads it when work happens in that subtree. The audit walks up to three
levels deep (skipping `node_modules`, `target`, `build` and friends) and
size-checks whatever it finds.
Only the root file gets the full treatment — Decisions & Learnings parsing,
staleness, coverage — because that is where the log lives and reporting on five
files at every session start would be its own kind of noise.
## The hooks
### SessionStart audit (`audit-claude-md.py`)
Fires once per session. Reads CLAUDE.md, identifies:
- D&L section >300 lines OR >35 entries → "compaction RECOMMENDED"
- D&L >200 lines OR >25 entries → "consider compaction"
- Any entry >800 chars → split or compact candidate
- Any topic tag with 3+ entries → graduation candidate
- **Staleness** (predicates in `freshness.py`, the shared vendorable core) — entries citing backticked artifacts `git grep` can no longer find; version pins a build file (`pom.xml`, `package.json`, `Cargo.toml`, `go.mod`, `pyproject.toml`) now contradicts; and "latest is `V27`"-style sequence facts where the tree holds a higher-numbered file. All grounded in the tree, silent when parsing is uncertain → strike/update candidates
- **Coverage gaps** — the one check that pushes *up* (see below)
- **Capture-side states** (issue #37) — `unadopted` (a hand-rolled gotchas/learnings section with bullets but zero parseable D&L entries → offer the migration); `empty log` (20+ commits, docs/ markdown ≥5× CLAUDE.md, no D&L entries — the learning is going somewhere that doesn't load every turn); docs **recurrence** ("the third time…" self-counting language in docs/ — a rule begging to graduate); **layout drift** (a git-tracked top-level dir CLAUDE.md never mentions, and the reverse: a mentioned `dir/` gone from the tree — the plain layout check is one-shot and satisfied forever, this is what keeps prose tracking the tree; foreign paths are filtered by requiring the dir to have existed in this repo's git history). Both drift directions name the **edit**, not just the finding — add the line, or strike it — because a flag a reader must translate into an action is a flag that gets skimmed past
Silent when healthy. When triggered, emits `hookSpecificOutput.additionalContext` so the assistant sees the recommendation and can propose action.
### PreToolUse lint (`lint-claude-md.py`)
Fires before any `Write|Edit` of CLAUDE.md. Reads the proposed content, validates that any new D&L entries have:
- Valid `YYYY-MM-DD` date prefix
- A `**topic-tag**` (bold, kebab-case)
- Body ≤500 chars (200 is the target — see Format)
If any entry violates, denies with a `reason` explaining which line + how to fix. The assistant retries with a corrected entry.
### Capture triggers (`capture-triggers.py`) — default OFF
The write path the other hooks never had. Two opt-in triggers
(`.claude/evolving-claude-md/config.json`), off until calibration measures
their noise (the retired gotchas check's 59%-fire lesson gates these too):
- **Session-end capture** (`"capture_prompt": "session-end"`, Stop hook) —
commits + edits but CLAUDE.md untouched and no `docs/decisions/` file →
block the stop once: nominate 0–3 entries. Zero is explicitly allowed.
- **Commit mining** (`"commit_mining": true`, PostToolUse on Bash) — a commit
message with gotcha-shaped language ("turns out", "silently", "reports
success") is a finished learning; suggest promoting it while context is hot.
Both prompts route: repo-durable, team-relevant → a D&L entry here;
machine-personal or private → the `learn-on-failure` skill to user memory.
One capture engine, two destinations — this plugin owns the triggers,
learn-on-failure owns the memory-side write path.
### Parallel lanes: spool, don't collide
N agents working one repo at once are N writers on one file, and the collision
is invisible until integration. Measured on one 2,400-commit repo running
worktree lanes: **86 of 257 non-merge `CLAUDE.md` edits were made on lane
branches, and all 50 merge commits touching the file were resolving them.**
So a lane never edits `CLAUDE.md`:
- **Lane** (a linked git worktree — the signal that siblings are running; or
any non-default branch with `"lane_spool": "branch"`). The session-end prompt
routes its nominations to `.claude/evolving-claude-md/incoming/<branch>.md`.
One file per lane, so distinct paths merge cleanly and the conflict class
disappears rather than being resolved. An ordinary feature branch is *not* a
lane by default: one writer at a time merges fine, and diverting it would be
a false positive.
- **Integrator**, once, on the default branch: `capture-triggers.py fold`
prints every lane's nominations together and flags near-duplicates across
lanes (five lanes on one epic learn the same lesson five times). Apply the
bar, write the surviving entries into `CLAUDE.md` as one writer — so the
lint hook still gates their format — then delete the folded files.
Folding is deliberately a human/main-session step, not another hook: only a
reader holding all the lanes at once can dedup them.
### PostCompact audit
Re-runs `audit-claude-md.py` after Claude Code compacts the conversation context. Same output shape as SessionStart. Keeps the assistant aware of CLAUDE.md state across a compaction without paying to re-read the whole file.
## Tuning the thresholds per repo
Every threshold and check in this skill is overridable per repo (and globally)
in `.claude/evolving-claude-md/config.json` — the full key list, defaults and
resolution order are in [references/setup-and-tuning.md](references/setup-and-tuning.md).
## Where an entry goes — CLAUDE.md vs `.claude.local.md`
`CLAUDE.md` is committed and shared; `.claude.local.md` is gitignored and yours.
Both load into context the same way, so the split is about *audience*, not size.
| Goes in `CLAUDE.md` | Goes in `.claude.local.md` |
|---|---|
| Decisions the team is bound by | How **your** machine happens to be set up |
| Conventions, gotchas, architecture | Absolute paths under your home directory |
| Anything true for every clone | Personal tokens, local ports, scratch dirs |
| Why a tradeoff was made | "the wrapper is broken on my box, I use `mvn`" |
The test is one question: **would this still be true on a teammate's laptop, in
CI, and in a fresh clone?** No means local.
Two things make this easy to get wrong. A machine-specific fact often *feels*
like a project fact when you're the only person working in the repo — and an
absolute path with your username in it is the most common way a personal detail
gets committed. `~/` is fine; `/home/alex/…` and `/Users/alex/…` are not.
When a learning is genuinely mixed — a real project decision plus a local
workaround — split it. The decision goes in the shared file with the reasoning,
the workaround goes local.
The audit size-checks `.claude.local.md` alongside the shared file, since it
costs the same context whichever file it sits in.
## Coverage — the one upward check
Every other check pushes content DOWN; this one asks whether the essentials are
there at all — a build file on disk with no matching command in CLAUDE.md, or a
many-directory repo with no layout prose. Grounded in the tree, never a generic
checklist. Full reasoning: [references/audit-checks.md](references/audit-checks.md).
## Structure review
The file's shape evolves with the project too: sections that outgrew their
heading, content in the wrong place, headings the tree no longer justifies.
Recommend, don't rewrite — and respect the measured hierarchy (presence ≫
brevity ≫ consistency ≫ structure) in
[references/claude-md-best-practices.md](references/claude-md-best-practices.md).
Procedure: [references/audit-checks.md](references/audit-checks.md).
## Quality bar for entries
Each entry passes all three:
- **Specific** — names the thing decided ("switched Mapbox → Leaflet"), not the area ("map work").
- **Sourced** — the *why* exists (constraint, incident, preference, tradeoff).
- **Actionable** — a future contributor can judge whether the entry still applies to a new edge case.
## Setup
Installed as a plugin the hooks come wired. Manual install, hook JSON and the
verification steps: [references/setup-and-tuning.md](references/setup-and-tuning.md).
## The quality gate
One more script ships beside the hooks: `test-harness.py`. It is not part
of the runtime and the hooks never call it — it is the release gate, run with
`make test-evolve` (or `python3 test-harness.py` from the skill directory)
after any change to the audit or lint scripts. Read it as documentation of the
checks' intended behaviour; run it before shipping a change to them. It covers
the coverage checks, the freshness detectors (fire and non-fire cases each),
the per-repo config resolution, companion-file discovery,
and the hook's exit-0/valid-JSON contract — with a pinned zero-false-positive
calibration across real repos.
## Common failure modes
- **The log accumulates without pruning.** The audit hook screams at you; act on it.
- **Mega-entries.** The lint hook blocks them at edit time. If you bypass and it lands anyway, the audit catches it.
- **Topic tag inconsistency.** Lint enforces presence; the audit surfaces clustering. Pick existing tags before inventing new ones.
- **Graduating too eagerly.** A pattern with 3 entries spread across one week isn't stable — wait 14 days minimum.
- **Skipping the archive.** Quarterly archive is operational; nothing automates it. Calendar reminder.
- **The log is never written to.** Every other failure mode assumes entries exist. If a session ends with commits but no new entry, the mechanism has failed *silently* — which is worse than failing loudly. A repo whose findings all live in `docs/` has this failure; the capture-side audit states and the (opt-in) capture triggers exist for exactly this.
## Boundary — this skill vs `memory-hygiene`
This skill governs `CLAUDE.md` and its companions: repo-scoped, committed,
human-authored with the agent proposing. The *other* half of the loaded
context — the agent-written memory under `~/.claude/projects/<slug>/memory/`
— belongs to the sibling `memory-hygiene` plugin. Different artifact,
different failure mode: bloat here makes instructions get ignored, rot there
makes the agent confidently recall something false. So this skill prunes
down; that one re-verifies and invalidates. They share one `freshness.py`
core (vendored in both) so "is this fact still true on disk?" has exactly
one implementation. "Make CLAUDE.md evolve" is this skill; "my memory has
gone stale" is that one.
Files in this skill
- SKILL.md
- archive-decisions.py
- audit-claude-md.py
- capture-triggers.py
- freshness.py
- lint-claude-md.py
- references/claude-md-best-practices.md
- test-harness.py
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.