Sai sót khi deploy không có triệu chứng. Bốn hàng rào — credentials tách riêng, script EDITOR, đọc lại xác minh, lật runtime theo env — để Claude hữu ích mà không thiêu mọi thứ.
Khác biệt lớn nhất giữa deploy và viết code: deploy là một lần, rủi ro cao, rollback khổ. Viết sai một dòng code, test chạy một lần là bắt được. Viết sai một dòng credentials, phải đợi đến lần thanh toán thật đầu tiên mới biết — lúc đó user đã quẹt thẻ, tiền không về tài khoản bạn, log toàn 400.
Gần đây tôi đưa how2claude từ dev local lên production cùng Claude: tài khoản Stripe live, ví x402 mainnet, Google OAuth, Kamal secrets. Claude không biết giá trị nào của test, giá trị nào là thật. Không biết sk_live_ và sk_test_ chỉ khác một chữ nhưng kết cục trời đất. Hàng rào bạn tự dựng.
Rails mặc định dùng config/credentials.yml.enc, key giải mã là config/master.key. Dùng mặc định này là bạn thua.
Nếu bạn nhét sk_live_xxx vào file đó rồi chạy test local, code test của bạn đang dùng Stripe production. Mỗi lần test chạy là một lần thẻ thật bị trừ.
Tách thành hai:
- config/credentials.yml.enc + config/master.key: dev/test, chứa sk_test_xxx
- config/credentials/production.yml.enc + config/credentials/production.key: prod, chứa sk_live_xxx
Cả hai .key đưa vào .gitignore, cả hai .enc commit. production.key chỉ nằm trên máy deploy.
Sau đó Kamal secrets phải trỏ đúng file:
# .kamal/secrets
-RAILS_MASTER_KEY=$(cat config/master.key)
+RAILS_MASTER_KEY=$(cat config/credentials/production.key)
Trong container, RAILS_MASTER_KEY giờ trỏ vào key production, và thứ được giải mã là credentials production. Tôi quan sát Claude viết dòng này — template mặc định của Kamal là config/master.key (của dev), nó âm thầm deploy Stripe key dev lên production.
bin/rails credentials:edit --environment production mở editor tương tác. Claude không điều khiển được editor tương tác. Bạn cũng không muốn dán tay hàng chục key (một typo là toang hết).
Dùng pattern này:
EDITOR="ruby script/set_prod_webhook_secret.rb" \
bin/rails credentials:edit --environment production
rm script/set_prod_webhook_secret.rb
script/set_prod_webhook_secret.rb trông thế này:
# ARGV[0] là path tới file YAML tạm đã giải mã
file = ARGV[0]
require "yaml"
data = YAML.load_file(file) || {}
data["stripe"] ||= {}
data["stripe"]["webhook_secret"] = "whsec_GHWObNAKFh2HPOlJpbGmlYfIiKz1C8EY"
File.write(file, data.to_yaml)
Rails ghi YAML đã giải mã ra file tạm, gọi "EDITOR" của bạn với path đó là đối số, rồi mã hoá lại khi thoát. "EDITOR" của ta thực ra là một script Ruby, sửa chính xác một key rồi lưu.
Ưu điểm:
- Chính xác: chỉ động vào stripe.webhook_secret, không động gì khác.
- Idempotent: chạy hai lần kết quả giống chạy một.
- Có thể kiểm toán: bản thân script là cái diff. Claude viết, bạn liếc qua, biết chính xác nó sẽ đổi cái gì.
- Xoá là mất: sau rm, không còn credential dưới dạng text trên disk, shell history cũng không có vết paste whsec_....
Mỗi credential một script: set_stripe_live_key.rb, set_webhook_secret.rb, set_price_ids.rb, set_wallet_address.rb. Xong mỗi cái là rm liền.
Viết xong, đừng tin rằng nó viết đúng. Đọc lại:
bin/rails runner -e production "
c = Rails.application.credentials
puts 'sk_live set: ' + c.dig(:stripe, :secret_key).to_s.start_with?('sk_live_').to_s
puts 'webhook_secret set: ' + c.dig(:stripe, :webhook_secret).to_s.start_with?('whsec_').to_s
puts 'wallet prefix: ' + c.dig(:x402, :wallet_address).to_s[0..5]
puts 'wallet len: ' + c.dig(:x402, :wallet_address).to_s.length.to_s
"
Điều quan trọng là kiểm tra prefix và độ dài, không phải chỉ in ra nhìn.
sk_live_. Nếu đọc lại thấy sk_test_, Claude đã nhét test key vào file prod — bug bạn sẽ không thấy cho đến lần thanh toán thật đầu tiên.whsec_. Đúng format hay không thấy liền.0x.... Sai độ dài nghĩa là có ký tự lạ lọt vào.Thêm kiểm tra prefix thôi đã chặn phần lớn typo, sót field, trộn môi trường.
Rất dễ nghĩ "trước khi deploy tôi đổi network sang mainnet" — kiểu chuyển này dựa vào trí nhớ người, sớm muộn cũng cháy.
Viết quy tắc vào initializer:
# config/initializers/x402.rb
X402.configure do |c|
c.wallet_address = Rails.application.credentials.dig(:x402, :wallet_address)
c.chain = Rails.env.production? ? "base" : "base-sepolia"
end
Cùng code, dev chạy testnet (sepolia), prod chạy mainnet (base). Deploy không cần sửa gì. Claude cũng không thể "quên chuyển" vì chuyển không phải việc của nó.
Chiêu tương tự cho basescan_tx_url, độ hiển thị Plan (plan dev-only không hiện ở prod), chọn Stripe price ID, v.v. Mọi thứ khác nhau giữa dev và prod đều lật bằng Rails.env. Đừng dựa vào nhớ lúc deploy.
Bốn hàng rào xong xuôi, lần đầu click thanh toán tiền thật vẫn phải là bạn.
Bài trước Để Claude gỡ những bug im lặng tôi kể: click x402 đầu tiên ở production, console báo liền invalid_string at payTo — ký tự thứ 43 của địa chỉ ví là dấu chấm hỏi fullwidth lọt vào từ IME tiếng Trung. Kiểm tra prefix không bắt được (0x vẫn còn), test không bắt được (test không phát giao dịch thật), chỉ click thật một lần mới lộ.
Hàng rào deploy không phải cho Claude. Là cho bạn — tự động hoá cái có thể tự động hoá (prefix, độ dài, lật env) để sự chú ý tiết kiệm được dồn cho những tương tác thật mà tự động hoá không phủ.
Flow đầy đủ của Claude-deploy:
.enc + .key riêng.EDITOR=script, xoá ngay.Rails.env, không chuyển tay lúc deploy.Deploy không phải "viết code với rủi ro cao hơn". Deploy là "viết code mà không ai báo bạn sai tức thì". Hàng rào biến "không ai báo" thành "bạn biết trong 15 giây".