Deploy errors come with no symptoms. Four guardrails — split vaults, EDITOR scripts, readback checks, env-based runtime flip — keep Claude useful without letting him burn things down.
The big difference between deploying and writing code: deployment is one-shot, high-stakes, and painful to roll back. Write a broken line of code, one test run catches it. Write a broken line of credentials, you only find out at the first real payment — users have charged their card, the money didn't land in your account, the logs are nothing but 400s.
I recently took how2claude from local dev to production with Claude: Stripe live account, x402 mainnet wallet, Google OAuth, Kamal secrets. Claude doesn't know which values are test and which are real. It doesn't know that sk_live_ vs sk_test_ is a one-letter difference with world-ending consequences. You build the guardrails.
Rails defaults to config/credentials.yml.enc, decrypted by config/master.key. If you use that default, you lose.
If you put sk_live_xxx in that file and then run tests locally, your test code is using production Stripe. Every test run charges a real card.
Split into two:
- config/credentials.yml.enc + config/master.key: dev/test, holds sk_test_xxx
- config/credentials/production.yml.enc + config/credentials/production.key: prod, holds sk_live_xxx
Both .key files in .gitignore, both .enc files committed. production.key lives only on the deploy machine.
Then Kamal secrets has to point at the right file:
# .kamal/secrets
-RAILS_MASTER_KEY=$(cat config/master.key)
+RAILS_MASTER_KEY=$(cat config/credentials/production.key)
RAILS_MASTER_KEY inside the container now points at the production key, and what gets decrypted are production credentials. I watched Claude write this line — the default Kamal template is config/master.key (the dev one), which silently deploys the dev Stripe key to production.
bin/rails credentials:edit --environment production opens an interactive editor. Claude can't drive an interactive editor. You don't want to manually paste a dozen secrets either (one typo bricks everything).
Use this pattern:
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 looks like:
# ARGV[0] is the path to the temp decrypted YAML file
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 writes the decrypted YAML to a temp file, calls your "EDITOR" with that path as an argument, then re-encrypts on exit. Your "EDITOR" is just a Ruby script that surgically modifies one key and saves.
Why it's good:
- Precise: only touches stripe.webhook_secret, nothing else.
- Idempotent: running twice is the same as running once.
- Auditable: the script is the diff. Claude writes it, you glance at it, you know exactly what it'll change.
- Vanishes on delete: after rm, there's no plaintext credential on disk, no whsec_... paste in your shell history.
One script per credential: set_stripe_live_key.rb, set_webhook_secret.rb, set_price_ids.rb, set_wallet_address.rb. rm each one right after.
After writing, don't trust that it wrote correctly. Read it back:
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
"
The key is prefix and length checks, not just printing the values.
sk_live_-prefixed. If you read back sk_test_, Claude put the test key in the prod file — a bug you won't find until the first real payment.whsec_-prefixed. Format right or wrong in a glance.0x.... Wrong length means some other character snuck in.Adding prefix checks blocks most typos, dropped fields, and mixed environments.
It's tempting to think "I'll change the network to mainnet before deploying." That kind of switch relies on human memory. Sooner or later it'll burn you.
Bake the rule into the 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
Same code, testnet (sepolia) in dev, mainnet (base) in prod. Nothing to change at deploy time. Claude can't "forget to switch" because switching isn't its job.
Same trick for basescan_tx_url, Plan visibility (dev-only plans don't render in prod), Stripe price ID selection, and so on. Anything that differs between dev and prod, flip with Rails.env. Don't rely on remembering at deploy time.
With all four guardrails in place, you still have to click the real-money payment yourself the first time.
In the previous article Debugging Silent Bugs with Claude, I described the first x402 payment click in production: invalid_string at payTo in the console. The 43rd character of the wallet address was a fullwidth question mark that snuck in from a Chinese IME. Prefix checks couldn't catch it (the 0x was still there), tests couldn't catch it (tests don't fire real transactions), only a real click surfaced it.
Deployment guardrails aren't for Claude. They're for you — automate what can be automated (prefixes, lengths, env flips) so the attention you save goes to the real interactions automation can't cover.
The full Claude-deploy flow:
.enc + .key files.EDITOR=script, delete it immediately.Rails.env, not at deploy time.Deployment isn't "writing code with higher stakes." Deployment is "writing code where nobody tells you you're wrong." The guardrails exist to turn "nobody tells you" into "you know within 15 seconds."