Free

A Good CLAUDE.md Doesn't Describe Features — It Only Captures What Claude Can't See From Reading the Code

A good CLAUDE.md isn't a README — it captures invariants Claude can't infer from code. 6 things to write, 4 to skip, 5 questions to ask.


My Pickful project has a decentralized community jury system, x402 crypto payments, Sign-In with Ethereum, a multi-database setup, real-time push — all bleeding-edge stacks from the last couple of years. Claude ships these features fast and well.

But open the project's CLAUDE.md and you'll notice: the jury system and x402 don't appear — not even once.

That's not an oversight. The purpose of CLAUDE.md has never been to "describe features." It's to capture the things Claude could never figure out just by reading the code.

Feature Descriptions Are Wasted Tokens

People writing CLAUDE.md for the first time often treat it like a README — describing every core feature:

  • "The jury system lets users report content; reported items go to a public vote where jurors..."
  • "x402 payments trigger on-chain transfers via the HTTP 402 status code..."
  • "Likes award points; at 400 points a user becomes VIP..."

Claude can open topic_review_service.rb / x402.rb / like_points_service.rb and read this more accurately than you'll ever write it. A thousand-word description of business logic costs hundreds of tokens for Claude to read from code — and doesn't introduce interpretation drift. Code is fact. Descriptions are second-hand.

What actually trips Claude up are these 6 categories.

The 6 Categories That Actually Save the Day

1. Counter-Intuitive Architectural Choices

Pickful's CLAUDE.md has lines like these:

Propshaft (not Sprockets)
ImportMap (no JavaScript bundler)
Hotwire: Turbo Frames, Turbo Streams, Stimulus
Lexxy gem overrides ActionText:
  config.lexxy.override_action_text_defaults = false

Every line is pushing back against the default guess. Looking at a Rails project, Claude's default assumptions are:

  • Assets go through Sprockets (the legacy default)
  • JS uses Webpacker or esbuild
  • Frontend is either React or some Stimulus + Turbo mix
  • Rich text is vanilla ActionText

Without these lines in CLAUDE.md, ask Claude to add a new JS feature and it may install Webpacker, edit package.json, write a bundler config — all wrong, and wrong in a way that doesn't fail loudly (the app still runs, but your asset pipeline is now polluted).

Those lines in CLAUDE.md tell Claude: don't guess, the decision is made.

2. Multi-Database Layout

PostgreSQL with 4 separate databases:
- primary - Main application data
- cache   - Solid Cache storage
- queue   - Solid Queue jobs
- cable   - Action Cable subscriptions

Plain phrasing, but it can save you an entire evening. Rails 8 default multi-db is a new behavior — Claude won't go check how many databases you have. A seemingly innocuous migration landing in the wrong database doesn't fail in development (all four are PostgreSQL — the schema migrates fine anywhere), but in production Solid Queue's job table leaks into the primary backup, or a primary model queries the cache database — bugs that take days to surface.

Two lines in CLAUDE.md vs. a full day of production debugging.

3. Invisible URL-Routing Conventions

/p-{slug} - Short post URLs (4-5 char alphanumeric)
/t-{slug} - Topic URLs (3-4 char alphanumeric)
/s-{code} - Short URL redirects (3-4 char alphanumeric)
/r-{referral} - Referral links

Claude can see the routes in routes.rb, but the length conventions (4-5 chars, 3-4 chars) are buried inside the slug-generation logic of models or services. Ask Claude to add a new short-link type and it'll probably generate a 6-character slug, a UUID-style string, or pure digits — visually out of step with your whole system.

These "conventions" share a trait: breaking them doesn't produce an error, but the code feels wrong to the next reader. Must be written.

4. Hard-Coded Business Thresholds

VIP status at 400+ points
Posts with 15+ likes are "hot" posts

Both numbers live somewhere in code (User#vip?, a Post#hot? scope). The problem: when Claude changes something adjacent — tweaking reward amounts, adding an "almost VIP" notification, writing a cron to pin hot posts — it won't automatically align the thresholds elsewhere.

Result: you reward a task with 500 points but the copy says "you can become VIP" (which is 400); or you seed data for a new feature with too few likes and nothing ever crosses the 15 threshold.

Claude's coding is strong, but it has no system-wide number sense. Putting key thresholds in CLAUDE.md means every conversation starts knowing "400 and 15 are special numbers."

5. Auth/Authz Stack Signpost

- Devise (authentication) + Pundit (authorization)
- Pundit policies in app/policies/
- Check UserPolicy, PostPolicy, etc. for permission rules

This line's job is navigation, not description.

Without it, when Claude needs to add a new permission check, three things can happen:

  • Hardcode unless current_user.admin? in a controller
  • Dig up a lingering CanCan remnant
  • Invent its own authorize? method in a model

With "Pundit policies in app/policies/" written down, Claude goes straight to app/policies/ to add a policy file, consistent style.

One line eliminates the "detective work" every time.

6. Project-Wide External Constraints

Supported locales: en, zh-CN, zh-TW
Testing stack: RSpec + FactoryBot + Capybara + Shoulda Matchers

Adding a new feature, Claude's defaults are:

  • Add English strings only
  • Write tests with Minitest + fixtures (Rails default)

But your project actually needs:

  • Translations in 3 locales
  • RSpec + FactoryBot, not fixtures

Violating these "project-wide external constraints" generates a lot of downstream cleanup — translations to backfill, tests to rewrite. CLAUDE.md nails down "things that must happen every time" once and for all.

The Other Side: These Are Pure Waste

Just as important as "must write" is "don't write." Delete the following on sight:

1. Feature descriptions

"Jury system: users can flag non-compliant content; flagged items enter a public vote; jurors are drawn from..."

→ Claude opens topic_review_service.rb and gets it more accurately than you wrote it. Cramming this into every new conversation's context is pure waste.

2. Things readable from the directory tree / Gemfile

"app/models/ contains ActiveRecord models", "Uses Rails 8", "Database is PostgreSQL"

→ Claude scans the project root and Gemfile and knows instantly.

3. Generic programming wisdom

"Controllers should be thin, delegate to services", "Avoid N+1 queries", "Write tests for main features"

→ This is already in Claude's training. Only write it when your project is unusual — e.g., "we deliberately don't use a service layer; logic lives in controllers."

4. Current-task context

"Right now we're refactoring the payment system; the focus is..."

→ That's conversation context, not project fact. Dropping it into CLAUDE.md pollutes every other conversation.

Audit Run: My Own CLAUDE.md Could Lose Half

Put the "I can do it" proof before the pitch. After writing the section above, I ran Pickful's CLAUDE.md — 238 lines — through the same 5 questions. Result: about half is waste.

What to cut (~120 lines):

Most of the dev commands section (70 lines → 10): bin/setup / bin/rails db:migrate / bundle exec rspec / bin/rubocop / bin/brakeman are all standard Rails — already in Claude's training. Keep only the three project-specific ones: bin/jobs (Solid Queue worker), bin/importmap pin (ImportMap-specific), bin/kamal deploy.

Core Domain Models list (35 lines → nuke it): listing 20 model names with one-line roles is the textbook "README-style CLAUDE.md." Claude runs ls app/models/ or reads one model file and knows. Shipping this into every conversation is pure waste.

Tech Stack boilerplate (28 lines → 8): Rails 8 / Devise / Pundit / Tailwind / pg_search all come from the Gemfile at a glance. Keep only the counter-intuitive ones: Propshaft / ImportMap / Lexxy / x402-rails / Grover.

Scattered generic programming wisdom: "Controllers should be thin", "Use app/jobs/ for async processing", what request specs vs. model specs each cover — Claude does these by default. Pure token overhead.

What remains (~100 lines): URL-routing conventions, 4-database layout, VIP 400 / Hot 15 thresholds, Lexxy override, counter-intuitive stack choices, Pundit signpost, locale list, FactoryBot (not fixtures).

But more importantly: which invariants are still missing?

While auditing I realized a few I should have added and never did:

  • Numbers hiding inside the jury system (voting threshold, juror eligibility rules) — not surfaced in CLAUDE.md at all
  • For x402, chain id / contract address / required env vars — without these, Claude won't grep the config file and will just invent values
  • Special rules in the point-trading / referral services

238 lines → 100–120 lines, plus 5–10 lines of previously missed invariants — that's closer to the "right density" CLAUDE.md.

This isn't a tutorial. It's an audit of my own project — and I got it wrong, too. The correct shape of CLAUDE.md is to keep deleting and keep adding — any project that matures for a bit should have a shorter, denser CLAUDE.md over time.

5 Questions Before Writing a Rule In

Every time I'm about to add something to CLAUDE.md, I run this checklist:

  1. Can Claude derive this rule from reading 3 files? If yes, don't write it — let it read.
  2. Is this rule counter-intuitive? (Unusual thresholds, non-mainstream library picks, config that contradicts upstream defaults.) If yes, it must be written.
  3. Is this rule a codebase-wide invariant, or does it only affect one file? One-file rules go in a code comment, not CLAUDE.md.
  4. Will breaking this rule silently cause Claude to do the wrong thing? (No error, just wrong semantics — wrong database, missing translation, skipped policy.) If yes, it must be written.
  5. Can you explain it in 3 lines? If not, you haven't thought it through yet — don't write it.

Rules that clear all 5 stay in. Any one that can't be answered — delete or rewrite.

The One-Line Takeaway

The right use of CLAUDE.md isn't "introducing the project." It's compressing the tacit knowledge between you and the codebase that Claude can never backfill by reading code — unusual picks, invisible thresholds, conventions that contradict defaults, project-wide external constraints.

Every line you add should answer: "Is this something Claude can't read out of the code?" Can't → keep. Can → delete.

A CLAUDE.md written this way is usually more than half the length of the first draft, but worth far more per conversation than thousands of words of feature description. And it's always "not done yet" — every new feature you ship surfaces another invariant that should have been in, and another paragraph that can now come out. Delete and add, delete and add — that's the daily maintenance of CLAUDE.md.