CLAUDE.md tốt không phải README — nó ghi các invariant Claude không thể suy ra từ code. 6 thứ cần viết, 4 thứ bỏ, 5 câu hỏi.
Dự án Pickful của tôi có hệ thống bồi thẩm cộng đồng phi tập trung, thanh toán tiền mã hóa x402, Sign-In with Ethereum, multi-database, push realtime — tất cả đều là các stack công nghệ mới xuất hiện trong một hai năm gần đây. Claude triển khai những tính năng này nhanh và sạch.
Nhưng mở CLAUDE.md của dự án ra, bạn sẽ thấy: hệ thống bồi thẩm và x402 — hai từ này không hề xuất hiện, dù chỉ một lần.
Đó không phải lỗi quên. Mục đích của CLAUDE.md chưa bao giờ là "mô tả tính năng" — nó là ghi lại những điều Claude không bao giờ suy ra được chỉ bằng cách đọc code.
Người viết CLAUDE.md lần đầu thường xử lý nó như README — mô tả từng tính năng cốt lõi:
Những nội dung kiểu này Claude mở topic_review_service.rb / x402.rb / like_points_service.rb ra là đọc chính xác hơn bạn viết. Một nghìn chữ mô tả logic nghiệp vụ, Claude đọc thẳng code tốn vài trăm token, và không có lệch diễn giải — code là sự thật, mô tả là thông tin tay hai.
Thứ thật sự khiến Claude vấp là 6 nhóm sau.
CLAUDE.md của Pickful có những dòng như thế này:
Propshaft (not Sprockets)
ImportMap (no JavaScript bundler)
Hotwire: Turbo Frames, Turbo Streams, Stimulus
Lexxy gem overrides ActionText:
config.lexxy.override_action_text_defaults = false
Mỗi dòng đều chống lại phán đoán mặc định. Nhìn một dự án Rails, phỏng đoán mặc định của Claude là:
Không có những dòng này trong CLAUDE.md, bảo Claude thêm tính năng JS mới, khả năng cao nó sẽ cài Webpacker, sửa package.json, viết config bundler — sai hết, mà sai một cách im lặng (app vẫn chạy, nhưng pipeline tài nguyên đã bị ô nhiễm).
Những dòng này trong CLAUDE.md đang bảo Claude: đừng đoán, đã quyết rồi.
PostgreSQL with 4 separate databases:
- primary - Main application data
- cache - Solid Cache storage
- queue - Solid Queue jobs
- cable - Action Cable subscriptions
Viết bình dị, nhưng cứu được nguyên một đêm debug. Multi-DB mặc định của Rails 8 là hành vi mới, Claude không tự đi kiểm tra bạn dùng bao nhiêu DB. Một migration có vẻ không liên quan hạ cánh sai DB, dev không báo lỗi (cả bốn đều là PostgreSQL, schema migrate ở đâu cũng được). Nhưng trên production, bảng job của Solid Queue lẫn vào backup của primary, hoặc model của primary query sang DB cache — loại bug này mất thời gian mới lộ ra.
Hai dòng trong CLAUDE.md đổi lấy một ngày truy bug trên production.
/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
Các route Claude có thể thấy trong routes.rb, nhưng quy ước độ dài (4-5 ký tự, 3-4 ký tự) chôn trong logic sinh slug ở model hoặc service. Bảo Claude thêm một kiểu short link mới, khả năng cao nó sinh slug 6 ký tự, kiểu UUID, hoặc chỉ số — lạc điệu với ngôn ngữ thị giác của cả hệ thống.
Đặc điểm của loại "quy ước" này: vi phạm không sinh lỗi, nhưng người đọc code sau sẽ thấy gợn. Phải viết.
VIP status at 400+ points
Posts with 15+ likes are "hot" posts
Cả hai con số đều nằm đâu đó trong code (User#vip?, scope Post#hot?). Vấn đề là khi Claude sửa tính năng liên quan — chỉnh thưởng điểm, thêm thông báo "sắp thành VIP", viết cron ghim hot post — nó không tự động đồng bộ ngưỡng ở nơi khác.
Kết quả: bạn thưởng 500 điểm cho một task nhưng copy nói "có thể trở thành VIP" (thực ra 400 là đủ); hoặc làm seed data cho tính năng mới với lượng like không đủ, không bao giờ chạm ngưỡng 15.
Năng lực code của Claude mạnh, nhưng nó không có cảm giác về các con số trên toàn hệ thống. Đặt các ngưỡng then chốt vào CLAUDE.md khiến mỗi đầu cuộc hội thoại nó đã biết "400 và 15 là số đặc biệt".
- Devise (authentication) + Pundit (authorization)
- Pundit policies in app/policies/
- Check UserPolicy, PostPolicy, etc. for permission rules
Vai trò của dòng này là điều hướng, không phải mô tả.
Thiếu dòng này, khi Claude cần thêm kiểm tra quyền mới, sẽ có ba khả năng:
unless current_user.admin? trong controllerauthorize? trong modelCó dòng "Pundit policies in app/policies/", Claude luôn vào app/policies/ thêm file policy, phong cách thống nhất.
Một dòng tiết kiệm "công việc thám tử" của Claude mỗi lần.
Supported locales: en, zh-CN, zh-TW
Testing stack: RSpec + FactoryBot + Capybara + Shoulda Matchers
Thêm tính năng mới, mặc định của Claude là:
Nhưng thực tế dự án bạn cần:
Vi phạm "ràng buộc ngoài phạm vi toàn dự án" kiểu này tạo ra hàng đống việc dọn dẹp phát sinh — dịch thiếu, test phải viết lại. Ghi vào CLAUDE.md là đóng đinh luôn "những việc phải làm mỗi lần".
Quan trọng ngang với "bắt buộc viết" là "đừng viết". Những nhóm sau, thấy = xóa:
1. Mô tả tính năng
"Hệ thống bồi thẩm: người dùng có thể báo cáo nội dung vi phạm, nội dung bị báo cáo sẽ vào giai đoạn bỏ phiếu công khai, bồi thẩm viên được chọn từ..."
→ Claude mở topic_review_service.rb đọc chính xác hơn bạn viết. Nhét đoạn này vào mọi cuộc hội thoại mới là lãng phí nguyên chất.
2. Những gì đọc được tức thì từ cấu trúc thư mục / Gemfile
"app/models/ chứa các ActiveRecord model", "Dùng Rails 8", "DB là PostgreSQL"
→ Claude liếc qua root dự án và Gemfile là biết ngay.
3. Kiến thức lập trình phổ quát
"Controllers should be thin, delegate to services", "Tránh N+1", "Viết test cho tính năng chính"
→ Đã có trong training set của Claude. Chỉ viết khi dự án của bạn bất thường — ví dụ "chúng tôi cố ý không dùng service layer, logic nằm trong controller".
4. Bối cảnh của task hiện tại
"Hiện tại chúng tôi đang refactor hệ thống thanh toán, trọng tâm là..."
→ Đây là bối cảnh hội thoại, không phải sự thật dự án. Nhét vào CLAUDE.md sẽ ô nhiễm tất cả các cuộc hội thoại khác.
Đặt bằng chứng "tôi làm được" trước lời kêu gọi. Sau khi viết phần trên, tôi quay lại chạy 5 câu hỏi của chính mình qua CLAUDE.md của Pickful — 238 dòng — kết quả: khoảng một nửa là lãng phí.
Cần cắt (~120 dòng):
Phần lớn block dev commands (70 dòng → 10): bin/setup / bin/rails db:migrate / bundle exec rspec / bin/rubocop / bin/brakeman đều là lệnh Rails chuẩn, đã có trong training. Chỉ giữ ba cái đặc thù dự án: bin/jobs (Solid Queue worker), bin/importmap pin (đặc thù ImportMap), bin/kamal deploy.
Danh sách Core Domain Models (35 dòng → xóa sạch): liệt kê 20 model với vai trò là điển hình "CLAUDE.md kiểu README" — Claude chạy ls app/models/ hoặc đọc một file model là biết. Nhét vào mỗi hội thoại là lãng phí thuần.
Mục chuẩn trong Tech Stack (28 dòng → 8): Rails 8 / Devise / Pundit / Tailwind / pg_search đều đọc được tức thì từ Gemfile. Chỉ giữ các mục phản trực giác: Propshaft / ImportMap / Lexxy / x402-rails / Grover.
Vài câu kiến thức lập trình phổ quát rải rác: "Controllers should be thin", "Use app/jobs/ for async processing", request specs / model specs test cái gì — Claude làm mặc định. Chỉ đốt token.
Còn lại ~100 dòng: quy ước routing URL, phân công 4 DB, hai ngưỡng VIP 400 / Hot 15, override Lexxy, chọn kiến trúc chống default, biển Pundit, danh sách locale, FactoryBot (không phải fixtures).
Nhưng quan trọng hơn: invariant nào vẫn chưa viết?
Trong quá trình audit tôi nhận ra vài cái nên thêm mà chưa bao giờ thêm:
238 dòng nén xuống 100–120 dòng, cộng thêm 5–10 dòng invariant trước kia bỏ sót — đó mới là CLAUDE.md gần "mật độ đúng".
Đây không phải tutorial, mà là cuộc audit với chính dự án của tôi. Tôi cũng chưa viết đúng. Hình thức đúng của CLAUDE.md là cứ xóa đi, cứ thêm vào — dự án càng trưởng thành, CLAUDE.md càng nên ngắn hơn, cứng hơn.
Mỗi lần định thêm gì vào CLAUDE.md, tôi đẩy nó qua checklist sau:
Các quy tắc vượt cả 5 câu thì giữ; câu nào không trả lời được — xóa hoặc viết lại.
Cách dùng đúng của CLAUDE.md không phải "giới thiệu dự án", mà là nén lại tri thức ngầm giữa bạn và codebase mà Claude không bao giờ bù đắp được qua đọc code — lựa chọn bất thường, ngưỡng vô hình, quy ước đi ngược default, ràng buộc ngoài phạm vi toàn dự án.
Mỗi dòng bạn thêm nên trả lời câu hỏi: "Điều này, Claude không đọc ra được từ code sao?" Không được — giữ. Được — xóa.
CLAUDE.md viết kiểu này thường ngắn hơn bản đầu hơn nửa, nhưng giá trị cho mỗi cuộc hội thoại gấp nhiều lần hàng ngàn chữ mô tả tính năng. Và nó luôn "chưa xong" — mỗi tính năng mới được đẩy đi lại lộ ra một invariant lẽ ra phải ở trong đó, và một đoạn cũ giờ có thể cắt. Xóa rồi thêm, xóa rồi thêm — đó là bảo trì hằng ngày của CLAUDE.md.