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.
People writing CLAUDE.md for the first time often treat it like a README — describing every core feature:
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.
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:
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.
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.
/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.
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."
- 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:
unless current_user.admin? in a controllerauthorize? method in a modelWith "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.
Supported locales: en, zh-CN, zh-TW
Testing stack: RSpec + FactoryBot + Capybara + Shoulda Matchers
Adding a new feature, Claude's defaults are:
But your project actually needs:
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.
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.
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:
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.
Every time I'm about to add something to CLAUDE.md, I run this checklist:
Rules that clear all 5 stay in. Any one that can't be answered — delete or rewrite.
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.