Every Claude Code session begins by loading files automatically, before your first message. Most people never look at what those files are or how big they've become. I finally did, and discovered huge files loading in every session and burning my tokens. I spent a day fixing it across all my projects.

This note is what I found and what I changed. It applies to Claude Code, ChatGPT Codex, or anything else that keeps a memory file in your project folder.

# The symptom

In one of my projects, the automatic load at session start was:

| What loaded | Size | Tokens |
|---|---:|---:|
| `context-log.md` — the whole file, via a hook that ran `cat context-log.md` | 447 KB | ~112,000 |
| `CLAUDE.md` | 32 KB | ~8,000 |

That's about **120,000 tokens spent before the session did anything** — more than half a 200,000-token context window, gone before I typed a word. And it fired again every time the conversation compacted and resumed.

A second project had the opposite problem. No session-start hook at all, so it loaded nothing — but its `CLAUDE.md` had grown to **110 KB (~27,700 tokens)** because every session kept appending its history to it.

Those look like different bugs. They're the same one: **nobody had ever decided what a session should know at startup**, so one project loaded everything and the other loaded nothing.

If you've been building with AI for a few months, you probably have one of these two. Go and check the size of your context log and your CLAUDE.md right now. That's the whole diagnostic.

# Why the files got that big

I traced one small bug fix — a single afternoon's work — and found it written down in **six places**: the context log, the technical documentation, the deploy log, two pull request descriptions and four commit messages.

None of those files ever shrink. Every session appends, nothing is ever removed, and the same fact ends up restated in files that then slowly disagree with each other as the code moves on. Six months later you have six accounts of the same event and no way to tell which one is still true.

The technical documentation had drifted furthest. It's *named* as a manual — how the system works now — but **62% of it was a dated changelog**: nineteen sections headed by a date, 181 KB of "on this date we changed X". Asking it "how does authentication work?" meant reading nineteen historical accounts and working out which one still applied.

# The rule everything else follows

> **One event, one home.**
>
> - **Context log** — what a session did.
> - **Technical documentation** — how the system works *now*. State, never history.
> - **Build history** — the narrative of each build phase.
> - **Deploy log** — deploys and what went wrong.
> - **CLAUDE.md** — standing instructions only. A rule that changes what a future session may do, or a trap that would otherwise cost another incident.
>
> **A file that accumulates dated `##` headings is in the wrong shape.**

That last line is the test you can apply in ten seconds. Open any of your project documents and look at the headings. If they're dates, it's a changelog — whatever the filename says.

# Fix 1: compute state, don't load a file

The session-start hook used to be `cat context-log.md`. It now runs a small script that **works out** the answer instead of reading it: current branch, how many files are uncommitted, the last three commits, the titles of the last eight sessions, and any open blockers.

Output is about **1.4 KB — roughly 350 tokens**, replacing 112,000. It never opens a log file.

Two details that turned out to matter more than they look:

- **Sort the session list by date, not by position in the file.** My first version used "take the first eight lines" and labelled the result "newest first". That was true in one project and *false* in another, where some sessions had appended to the bottom instead of the top. Copied across unchanged, it would have shown a confidently wrong list every single session — the worst kind of bug, because nothing looks broken.
- **Read the whole file family, not one filename.** Once you start archiving old logs (below), a hook that only knows about `context-log.md` goes blind to everything else.

The second hook runs when a session stops. It measures what the session actually wrote and says one of three things: nothing was written; more than 4 KB was written (about twice the "5–8 bullets" rule — this catches the 72 KB and 28 KB entries already sitting in my logs); or the live log has passed 150 KB and it's time to archive, naming the exact file to create.

**Both hooks only print. Neither blocks.** A blocking stop-hook can trap the AI in a loop, and the diagnostic is the whole point. The scripts and setup are in [[Hooks]].

# Fix 2: never read a log whole

Even with no auto-loading, the AI will still happily read a 250 KB log file — about 62,000 tokens — to find one paragraph. So the instruction in every one of my CLAUDE.md files is now:

1. `grep -n '^## ' context-log*.md` — that's the index. Every entry is a dated heading, and it regenerates itself, so it can never go stale.
2. Read only that line range. Or search by topic with `grep -n -i '<term>' context-log*.md` and read around the hits.

You don't need to understand `grep`. You need your CLAUDE.md to tell the AI to do this, because left alone it will just read the file.

# Fix 3: rotate the log, and watch two silent traps

When the live log passes about 150 KB, move the oldest complete quarter into `context-log-2026-Q2.md` and leave the newest in place. Only the live file ever gets appended to; archives are frozen.

Put archives **flat, beside the original, never in a subfolder.** My log had 46 links pointing at real files elsewhere in the project. Moving entries into a subfolder changes the relative depth of every one of those links and breaks them all. A sibling file changes nothing.

Two things will bite you on the first rotation, and **both fail silently** — which is why I'm spelling them out:

- **A `.gitignore` that names the log by exact filename.** Mine used a deny-everything-then-allow-specific-files pattern, and the allowance said `context-log.md`. A new `context-log-2026-Q2.md` would have been **invisible to git** — no error, no warning, the file simply never gets saved to GitHub, and three months of history quietly doesn't exist.
- **A link checker that excludes the log by exact filename.** Old log entries legitimately contain broken links, because they point at files that have since moved — that's the historical record being honest. Move those entries into a file with a new name and the exclusion no longer covers them, so your automated tests go red on a change that only moved text around.

There's a third, subtler one. After you rotate, `grep '^## ' context-log.md` keeps working perfectly, keeps returning results, and **quietly stops covering the archived quarters**. You'd never know you were reading a partial history. So every instruction moved to the star form, `context-log*.md`, with the words **"the `*` is load-bearing"** written next to it.

# Fix 4: make it a test, not a note to self

This is the part I'd most want a friend to take away.

I have a list of hard-won traps in my project instructions, and the first entry on it is: *a control that reads as enforcement and executes nothing has shipped here repeatedly.* A safety rule that only exists as a sentence in a document is a rule that gets skipped the moment a session is busy.

So each rule above became an automated check that runs on every change:

| Guard | What it catches |
|---|---|
| A `.gitignore` pattern instead of a filename | An archive silently never saving to GitHub |
| A link test | Every link in every document resolving to a real file |
| A document-shape test | A dated heading re-entering the technical manual |
| A workflow that actually runs the two tests | The above two existing but never executing |

That last row is not padding. My main test workflow was configured to skip whenever a change only touched documents — sensible for saving time, except it meant **a documentation check added there would be skipped on exactly the changes that break documentation.** A check that appears green because it never ran. That's the same failure the whole list is about, and it very nearly shipped again *inside the change meant to prevent it.*

Test your guards by deliberately breaking something and confirming they go red. Every one of mine was checked that way. Two of them didn't work the first time.

# The mistake I nearly made

My plan for the bloated technical documentation was simple: delete the changelog, 294 KB becomes about 110 KB.

Before deleting I counted what was actually in there. **296 of the 347 code references in the whole document appeared *only* in the dated sections** — API endpoints, admin pages, workflow files, database migrations. The changelog wasn't clutter sitting next to the manual. For most of the app, the changelog *was* the manual.

Deleting it would have destroyed documentation and called it tidying. So I stopped and re-planned. What I actually did was reorganise: take each dated section one at a time, fold what's still true into the relevant timeless section in present tense, move why-it-changed into the build history, and delete only what's genuinely superseded.

The file went from 294 KB to 280 KB. **The tiny size reduction is the point.** Nineteen dated headings became zero. One subject now has one answer instead of nineteen accounts you have to reconcile, and a test keeps it that way.

If you take one habit from this note: **measure before you delete.** "Obviously redundant" is a hypothesis, not a fact.

# What this actually bought me

- **Session startup: ~120,000 tokens → ~350.** That's most of a context window handed back to real work, on every session and every resume.
- **Reading cost is now capped and doesn't grow.** Rotation holds the live log at 150 KB and the stop-hook says when the cap is hit, so it happens rather than being noticed six months late.
- **Search works.** Grep a dated index and read one range, instead of reading half a megabyte to find a paragraph.
- **Documents that contradict each other are the exception.**
- **The rules are enforced, not remembered.**

Two live bugs surfaced along the way that I'd never have found otherwise: 300 broken links in one project, in a repo that had no way to see them, and a test exclusion in another that would have turned the build red the first time I archived anything.

# Do this today

1. Check the size of your `CLAUDE.md` and your context log. That number is your problem, or it isn't.
2. Look at whether anything runs at session start, and what it loads.
3. Move history out of `CLAUDE.md` into the file that owns it. Standing instructions only.
4. Add the two hooks from [[Hooks]].
5. Tell your AI, in `CLAUDE.md`, to grep the index and read one range — never the whole file.

The starting point for step 3 is my [[Sample CLAUDE.md file]].
