Los errores de deploy no dan síntomas. Cuatro barandas — vaults separados, scripts EDITOR, verificación por relectura, switch en runtime por env — para usar a Claude sin que queme todo.
La gran diferencia entre desplegar y escribir código: el despliegue es de un solo tiro, alto riesgo, y doloroso de revertir. Escribe una línea de código rota, un test la atrapa. Escribe una línea de credentials rota, solo te enteras en el primer pago real — los usuarios ya cobraron su tarjeta, el dinero no llegó a tu cuenta, los logs son nada más que 400s.
Recientemente llevé how2claude de dev local a producción con Claude: cuenta Stripe live, wallet x402 mainnet, Google OAuth, Kamal secrets. Claude no sabe cuáles valores son de test y cuáles son reales. No sabe que sk_live_ vs sk_test_ es una diferencia de una letra con consecuencias apocalípticas. Las barandas las pones tú.
Rails viene por defecto con config/credentials.yml.enc, descifrado por config/master.key. Si usas ese default, pierdes.
Si pones sk_live_xxx en ese archivo y luego corres tests locales, tu código de test está usando Stripe producción. Cada corrida cobra una tarjeta real.
Divide en dos:
- config/credentials.yml.enc + config/master.key: dev/test, guarda sk_test_xxx
- config/credentials/production.yml.enc + config/credentials/production.key: prod, guarda sk_live_xxx
Los dos .key en .gitignore, los dos .enc commiteados. production.key vive solo en la máquina de deploy.
Luego Kamal secrets tiene que apuntar al archivo correcto:
# .kamal/secrets
-RAILS_MASTER_KEY=$(cat config/master.key)
+RAILS_MASTER_KEY=$(cat config/credentials/production.key)
Dentro del container RAILS_MASTER_KEY ahora apunta a la clave de producción, y lo que se desencripta son credentials de producción. Vi a Claude escribir esta línea — la plantilla default de Kamal es config/master.key (la de dev), que despliega silenciosamente el Stripe key de dev a producción.
bin/rails credentials:edit --environment production abre un editor interactivo. Claude no puede manejar un editor interactivo. Tampoco quieres pegar manualmente una docena de secrets (un typo y se rompe todo).
Usa este patrón:
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 se ve así:
# ARGV[0] es el path al archivo YAML temporal desencriptado
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 escribe el YAML desencriptado en un archivo temporal, llama a tu "EDITOR" con ese path como argumento, y re-encripta al salir. Tu "EDITOR" es solo un script Ruby que modifica quirúrgicamente una clave y guarda.
Por qué funciona:
- Preciso: solo toca stripe.webhook_secret, nada más.
- Idempotente: correrlo dos veces es igual que una.
- Auditable: el script es el diff. Claude lo escribe, le echas un vistazo, sabes exactamente qué va a cambiar.
- Desaparece al borrar: tras rm, no hay credential en texto plano en disco, ni paste de whsec_... en tu shell history.
Un script por credential: set_stripe_live_key.rb, set_webhook_secret.rb, set_price_ids.rb, set_wallet_address.rb. rm a cada uno justo después.
Después de escribir, no confíes en que se escribió correctamente. Léelo de vuelta:
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
"
La clave son los chequeos de prefijo y longitud, no solo imprimir los valores.
sk_live_. Si lees sk_test_, Claude puso el test key en el archivo prod — un bug que no vas a encontrar hasta el primer pago real.whsec_. Formato correcto o no, se ve al instante.0x.... Longitud incorrecta significa que se coló algún otro carácter.Agregar chequeos de prefijo bloquea la mayoría de typos, campos caídos, y ambientes mezclados.
Es tentador pensar "cambio la network a mainnet antes de desplegar". Ese tipo de switch depende de la memoria humana. Tarde o temprano te quema.
Hornea la regla en el 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
El mismo código, testnet (sepolia) en dev, mainnet (base) en prod. Nada que cambiar al desplegar. Claude no puede "olvidar de cambiar" porque cambiar no es su trabajo.
Mismo truco para basescan_tx_url, visibilidad de Plan (planes dev-only no se renderizan en prod), selección de Stripe price ID, etc. Cualquier cosa que difiera entre dev y prod, conmuta con Rails.env. No dependas de acordarte al momento de desplegar.
Con las cuatro barandas en su lugar, aún tienes que clickear el pago de dinero real tú mismo la primera vez.
En el artículo anterior Depurar bugs silenciosos con Claude, describí el primer click de pago x402 en producción: invalid_string at payTo en la consola. El carácter 43 de la dirección de wallet era un signo de interrogación de ancho completo que se coló desde un IME chino. Los chequeos de prefijo no podían atraparlo (el 0x seguía ahí), los tests no podían atraparlo (los tests no disparan transacciones reales), solo un click real lo hizo aparecer.
Las barandas de despliegue no son para Claude. Son para ti — automatiza lo que puede automatizarse (prefijos, longitudes, switches por env) para que la atención que ahorras vaya a las interacciones reales que la automatización no cubre.
El flujo completo de Claude-deploy:
.enc + .key distintos.EDITOR=script, bórralo inmediatamente.Rails.env, no al momento de desplegar.El despliegue no es "escribir código con mayores consecuencias". El despliegue es "escribir código donde nadie te dice que estás equivocado". Las barandas existen para convertir "nadie te dice" en "te enteras en 15 segundos".