Free

Một CLAUDE.md tốt không mô tả tính năng — nó chỉ ghi những gì Claude không thể thấy khi đọc code

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ẩmx402 — 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.

Viết mô tả tính năng = đốt token

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:

  • "Hệ thống bồi thẩm cho phép người dùng báo cáo nội dung; 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ừ..."
  • "Thanh toán x402 kích hoạt chuyển tiền on-chain qua mã trạng thái HTTP 402..."
  • "Like tặng điểm; 400 điểm thành VIP..."

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.

6 nhóm thật sự cứu mạng

1. Lựa chọn kiến trúc phản trực giác

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à:

  • Tài nguyên tĩnh qua Sprockets (quán tính của dự án cũ)
  • JS dùng Webpacker hoặc esbuild
  • Frontend hoặc React hoặc trộn Stimulus + Turbo
  • Rich text là ActionText gốc

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.

2. Phân công giữa nhiều database

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.

3. Những "quy ước vô hình" của routing URL

/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.

4. Ngưỡng nghiệp vụ hardcode

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".

5. Biển chỉ đường cho stack auth/authz

- 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:

  • Viết thẳng unless current_user.admin? trong controller
  • Đào lên một mẩu CanCan còn sót lại, không còn dùng
  • Tự phát minh method authorize? trong model

Có 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.

6. Các ràng buộc ngoài phạm vi toàn dự á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à:

  • Chỉ thêm chuỗi tiếng Anh
  • Viết test bằng Minitest + fixtures (mặc định Rails)

Nhưng thực tế dự án bạn cần:

  • Dịch ra 3 locale
  • RSpec + FactoryBot, không phải fixtures

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".

Mặt sau: viết những cái này là lãng phí

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.

Audit thực chiến: CLAUDE.md của chính tôi cũng có thể cắt bớt một nửa

Đặ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:

  • Các con số chôn trong hệ thống bồi thẩm (ngưỡng bỏ phiếu, yêu cầu đủ điều kiện làm bồi thẩm) — hoàn toàn chưa lộ ra trên CLAUDE.md
  • Nếu x402 có chain id, địa chỉ contract, env var bắt buộc — không có, Claude sẽ không đi đọc file config và sẽ bịa giá trị
  • Các quy tắc ràng buộc đặc biệt trong service trao đổi điểm / Referral

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.

5 câu hỏi trước khi thêm một quy tắc

Mỗi lần định thêm gì vào CLAUDE.md, tôi đẩy nó qua checklist sau:

  1. Claude đọc 3 file có suy ra được quy tắc này không? Có — đừng viết, để nó đọc.
  2. Quy tắc này có phản trực giác không? (Ngưỡng bất thường, chọn thư viện lệch dòng chính, config đi ngược default upstream.) Phản trực giác — bắt buộc viết.
  3. Nó là invariant phạm vi codebase, hay chỉ ảnh hưởng một file? Một file — viết thành comment code, đừng leo lên CLAUDE.md.
  4. Vi phạm quy tắc này có khiến Claude lặng lẽ làm sai không? (Không báo lỗi nhưng sai ngữ nghĩa — sai DB, thiếu dịch, bỏ sót policy.) Có — bắt buộc.
  5. Có thể giải thích trong 3 dòng không? Không — chính bạn chưa sáng rõ. Chưa viết vội.

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.

Tóm một câu

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.