Skip to content

Bloat begone: Eliminating cruft from the codebase

How the BLOAT was banished, and what it takes to maintain a lean, mean, blogging machine.

· 5 min read

Two posts back, I wrote about tackling the bloat in standing instructions for AI agents. Where we left it: the amorphous mass of commentary cruft was shifted elsewhere, not eliminated, and it was still growing. It lurked like a fatberg in the sewers beneath this blog.

Being something of a pedant with obsessive-compulsive tendencies, I couldn't let that outcome sit. I kept nagging and pestering Claude until we could figure out a more permanent solution. Seriously, I just couldn't let it go. If my AI agent had a mouth, it would scream.

So I'm happy to report that the file that logs the arguments behind the rules is now less than half the size it was. The decisions log is now 1,058 lines long. A week ago it was 2,447. The core documentation survived, and now there's a test to stop it growing back.

Previously on Be Useful

Recapping the saga, the instructions file that Claude reads at the start of every session had grown to 1,734 lines. This was bad because it was nibbling away at my precious tokens – 25,000 and counting – which hampered how much work could be done in a single session.

So what we did was cleave the file in two. The rules stayed in CLAUDE.md, held to 280 lines by a test. The arguments behind the rules moved over to docs/decisions.md, a log that Claude only opens when a rule needs further context and explanation.

That kept every session cheap. So it was a victory, of sorts. But the log had no limit, and at the time of publication it was 2,260 lines and still growing. The obvious next step was to automate routine audits of those files.

Counting the words

The audit is a script. npm run measure:bloat reads the instructions file, the decisions log and every comment scattered across the codebase (which had a habit of parroting the log), then prints a report.

It checks how close the instructions file is to its 280-line limit. It lists the longest entries in the log. And it looks for repetition, by sliding an eight-word window along every document and flagging any comment that repeats a run of words from the log. A comment that copies an argument from the log is redundant and has to go.

But before the script gets round to doing any of that, it has to test itself. It takes 20 real words from the log and checks that it spots them, then feeds in a passage of made-up nonsense and checks that it spots nothing. A script that has stopped working and a codebase with nothing wrong in it would otherwise print the same clean report.

The Shingles function
const N = 8; // words per shingle

function shingles(ws) {
  const set = new Set();
  for (let i = 0; i + N <= ws.length; i++)
    set.add(ws.slice(i, i + N).join(" "));
  return set;
}

// Lift 20 real words from the log — the detector must find them.
// Feed in nonsense — it must find nothing.
const lifted   = words(decText).slice(400, 420).join(" ");
const invented = "zephyr marmalade quantum lighthouse accordion tangerine velvet orbit";

const hit  = [...shingles(words(lifted))].filter((k) => decisions.has(k)).length;
const miss = [...shingles(words(invented))].filter((k) => decisions.has(k)).length;
// Self test passed: 13 copied shingles found, 0 invented.

With the script up and running, it found the same arguments written out multiple times, once in the log and again in comments across four files. Replacing each copy with a pointer to the log removed 42 lines.

It kept growing, though

Removing duplicates didn't stop the growth, unfortunately. A few days later, the log was 2,447 lines.

At the roughly four characters per token from the last post, that's about 34,000 tokens, more than the instructions file that started all this. Granted, decisions.md isn't going to decimate the token budget in the same manner as CLAUDE.md upfront, but when called upon it still takes a bite.

The script could measure the problem, evidently, but it couldn't say what the log should look like instead.

Time for a new rule

Going back to the fundamentals, the problem was written into the log's own first paragraph.

It described every entry as "a decision someone had to defend, kept in full". CLAUDE.md sent it everything that wouldn't fit in a few lines. So the log was where all the overflow went, with an instruction to keep the whole story and no limit on size.

So the rule had to be replaced with a new one, stating more carefully what an entry is for.

An entry states the decision, the reason and what would reopen it, in about 20 lines. How it got there … is history and lives in git.

Comments in the code got the same treatment, too. A comment is now a line or two. It says only what the code can't, such as a trap or a hidden constraint, or it points to the log entry that holds the argument.

The key realisation is that the whole story of each decision was already saved. Git, the version history behind the code, keeps every earlier state of every file. The log had been keeping a second copy of all of it, and it really didn't need to.

decision-entry-paging
One entry from the decisions log, before and after. The grey is still in git.

Making the cuts

Thus armed with the new rule, we reviewed the log and the code comments and cut them back. 4,370 lines came out and 1,359 went back in.

decisions.md went from 2,447 lines to 1,058, even though all 60 decisions are preserved within it.

The median entry went from 34 lines to 16, and the longest from 127 to 41. In the code, the worst repetition of the log went from 73 matching runs of words to 11.

npm run measure:bloat

Before

After

decisions.md

2,447 lines

1,058 lines

Longest entry

127 lines

41 lines

Median entry

34 lines

16 lines

Worst comment duplication

73 shared runs

11 shared runs

Comment lines about old behaviour

45 across 26 files

30 across 14 files

A limit for the log

Next, we carried over a guardrail from CLAUDE.md. The instructions file stayed small because a test enforced its limit to 280 lines. The log didn't have one, but now it does.

The build fails if the log goes past 1,058 lines, the length it was cut to, and that number is only allowed to come down. A new entry has to be justified by trimming away an old one. Granted, this arbitrary limit might chafe if or when the Be Useful project grows in scope, but I'll address that later if I have to.

A second test checks that every comment pointing to a log entry points to one that exists, so trimming an entry can never leave the code pointing at nothing.

Lean and mean

The instructions file is at its 280-line limit and holding. The log is at 1,058 of 1,058, so the next decision that needs to be written down will have to replace something.

And reading the log now costs about 13,000 tokens, down from an estimated 34,000 a week ago.

That means both CLAUDE.md and decisions.md will be more efficient going forward, and a simple blog isn't dragging around a codebase's worth of cruft.

Documentation should preserve the information future collaborators need, not the information that explains every step by which the present was reached. The fatberg is gone.

The Fatberg
A dynamic, gritty comic book splash panel of a heroic worker in a scuffed yellow hazmat suit hydro-blasting a monstrous anthropomorphic fatberg. The creature is an urban garbage golem: its face and body are formed from congealed grease, tangled white wet wipes, flattened soda cans, plastic forks for teeth, and discarded plastic bottles. Chunks of congealed lard, bottle caps, and greasy packaging fly off as a high-pressure water blast hits it. Deep black inking, intricate crosshatching, vintage halftone screentone dots, cracked Victorian sewer tiles covered in faded tags and grime.

About the author

Read Next