好的 CLAUDE.md 不描述功能,只写 Claude 读代码补不齐的 invariant:6 类必写、4 类别写、5 问清单。
我的 Pickful 项目有去中心化社区陪审系统、x402 加密货币支付、Sign-In with Ethereum、多数据库、实时推送——全是这两年才起来的技术栈。Claude 做这些功能做得又快又好。
但打开项目的 CLAUDE.md,你会发现:陪审系统和 x402,连词都没出现过。
这不是疏忽。CLAUDE.md 的作用从来不是"描述功能",而是写那些 Claude 读一遍代码永远也看不出来的东西。
第一次写 CLAUDE.md 的人,常常像在写 README:把每个核心功能都描述一遍。
这些内容 Claude 打开 topic_review_service.rb / x402.rb / like_points_service.rb 就能比你写得更准。你用一千字描述的业务逻辑,Claude 读代码只花几百 token,而且不会产生理解偏差——代码是事实,描述是二手信息。
真正会让 Claude 翻车的,是下面这 6 类内容。
Pickful 的 CLAUDE.md 里有这么几行:
Propshaft (not Sprockets)
ImportMap (no JavaScript bundler)
Hotwire: Turbo Frames, Turbo Streams, Stimulus
Lexxy gem overrides ActionText:
config.lexxy.override_action_text_defaults = false
每一行都在对抗默认猜测。Claude 看到一个 Rails 项目,默认会假设:
如果不写进 CLAUDE.md,让 Claude 加个新 JS 功能,它很可能去装 Webpacker、改 package.json、写 bundler 配置——全错,而且错得很隐蔽(项目能跑,但资源管线就污染了)。
写进 CLAUDE.md 的这几行,就是在告诉 Claude:别猜,已经选好了。
PostgreSQL with 4 separate databases:
- primary - Main application data
- cache - Solid Cache storage
- queue - Solid Queue jobs
- cable - Action Cable subscriptions
这条写法朴素,但省掉的坑可能是整晚。Rails 8 默认多库是新行为,Claude 不会主动去查你用几个库。一个看似无关的 migration 放错数据库,开发时不报错(四个库都是 PostgreSQL,schema 写哪个库都能 migrate 通过),但生产上 Solid Queue 的 job 表混进 primary 备份、或者 primary 的模型跑去查 cache 库——这种错要跑一阵子才会浮现。
写进 CLAUDE.md 只要两行,定位一次线上故障要花一整天。
/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 读 routes.rb 确实能看到,但长度约定(4-5 char、3-4 char)藏在模型或 service 的 slug 生成逻辑里。让 Claude 加个新短链类型,它大概率会生成一个 6 位、UUID 风格或者纯数字的 slug——和你整个系统的视觉语言脱节。
这类"惯例"的特点:违反不会报错,但会让后来人看代码觉得不对劲。必须写。
VIP status at 400+ points
Posts with 15+ likes are "hot" posts
这两个数字都在代码里某处(User#vip?、Post#hot? scope)。问题是 Claude 改一个相关功能时——调整积分奖励、加一个"即将成为 VIP"的通知、写一个"热帖置顶"的定时任务——它不会主动把其他地方的阈值对齐。
结果:你给完成某个任务奖励 500 分,但文案写"可以成为 VIP"(实际 400 就是了);或者给新功能做种子数据时少给了赞,永远触不到 15 的门槛。
Claude 的代码能力很强,但它没有整个系统的数字感。把关键阈值放在 CLAUDE.md 里,让它每次对话开局就知道"400 和 15 是特殊数字"。
- Devise (authentication) + Pundit (authorization)
- Pundit policies in app/policies/
- Check UserPolicy, PostPolicy, etc. for permission rules
这条的作用是导航,不是描述。
没有这条,Claude 需要加个新权限控制时有三种可能:
unless current_user.admin? 硬写authorize? 方法写上"Pundit policies in app/policies/"这一行,Claude 每次都会直接去 app/policies/ 加 policy 文件,风格统一。
一行字省掉 Claude 每次的"侦探工作"。
Supported locales: en, zh-CN, zh-TW
Testing stack: RSpec + FactoryBot + Capybara + Shoulda Matchers
加一个新 feature 时,Claude 默认会:
而你的项目实际需要:
这种"项目范围的外部约束"违反了会产生一堆细碎的后续工作——翻译要补、测试要重写。写进 CLAUDE.md 就是一次性把"每次都必须做的事"钉死。
和"必须写"同样重要的是"别写"。下面这几类每次看到都删掉:
1. 功能描述
"陪审系统:用户可以举报违规内容,被举报的内容会进入公开投票阶段,陪审员从..."
→ Claude 打开 topic_review_service.rb 能读出来,而且读得比你写的更准。每次开新对话都把这段塞进上下文是纯浪费。
2. 能从目录结构 / Gemfile 秒读出的东西
"app/models/ 放 ActiveRecord 模型"、"使用 Rails 8"、"数据库用 PostgreSQL"
→ Claude 扫一眼项目根目录和 Gemfile 就知道了。
3. 通用编程常识
"Controllers should be thin, delegate to services"、"避免 N+1 查询"、"写测试覆盖主要功能"
→ 这些在 Claude 的训练集里。只有当你的项目反常时才写——比如"我们故意不写 service 层,逻辑直接放在 controller"。
4. 当前任务的上下文
"现在我们在做支付系统的重构,重点是..."
→ 这是对话上下文,不是项目事实。塞进 CLAUDE.md 会污染所有其他对话。
说服力要放在"我能做到"之前。写完上面这段,我回头用自己的 5 个问题过了一遍 Pickful 的 CLAUDE.md——238 行——结果大概有一半是浪费。
该砍的(约 120 行):
开发命令一整段的大部分(70 行 → 10 行):
bin/setup / bin/rails db:migrate / bundle exec rspec / bin/rubocop / bin/brakeman 全是标准 Rails 命令,Claude 训练集里的东西。只留项目特有的三条:bin/jobs(Solid Queue worker)、bin/importmap pin(ImportMap 专属)、bin/kamal deploy。
Core Domain Models 列表(35 行 → 删光):
把 20 个 model 名字和职责列一遍是最典型的"README 式 CLAUDE.md"——Claude 跑 ls app/models/ 或读一个 model 文件就知道,每次对话塞进去全是浪费。
Tech Stack 里的标准项(28 行 → 8 行):
Rails 8 / Devise / Pundit / Tailwind / pg_search 都能从 Gemfile 秒读。只留反直觉的:Propshaft / ImportMap / Lexxy / x402-rails / Grover。
通用编程常识散落的几条:
"Controllers should be thin"、"Use app/jobs/ for async processing"、request specs / model specs 各自测什么——这些 Claude 默认就会做,写了只是占 token。
留下来的大概 100 行:URL 路由惯例、4 数据库分工、VIP 400 / Hot 15 两个硬阈值、Lexxy override、反默认的架构选型、Pundit 导航、Locale 列表、FactoryBot(not fixtures)。
但更关键的是:还要补哪些没写的 invariant?
审计过程中我意识到几条应该加但一直没加的:
238 行压到 100–120 行、再补 5–10 行之前漏掉的关键 invariant——这才是更接近"正确密度"的 CLAUDE.md。
这不是教程,是一次对自己项目的审计。我也没写对。CLAUDE.md 的正确形态就是不停地删、不停地补——任何一个项目成熟一点,它应该越来越短、越来越硬。
我每次想往 CLAUDE.md 加内容,会按这个清单过一遍:
5 条都过的规则留下来;有一条答不上来的,删掉或重写。
CLAUDE.md 的正确用法不是"介绍项目",是压缩你和项目之间那些 Claude 无论怎么读代码都补不齐的隐性知识——反常的选型、隐形的阈值、违反默认的约定、项目范围的外部约束。
写进去的每一行都应该在回答一个问题:"这件事,Claude 读代码读不出来吗?" 读不出来——留下。能读出来——删掉。
这样写出来的 CLAUDE.md 通常比初版短一半以上,但对 Claude 每一次对话的价值,比几千字的功能描述大得多。而且它永远"还没写对"——每做一个新功能,都会发现之前漏掉的 invariant 该加、原来写的某段已经能砍了。不停地删、不停地补,就是 CLAUDE.md 的日常维护。