Free

Depurar bugs silenciosos con Claude

Tres bugs reales en los que el clic no hace nada — Claude falla cada vez hasta que una frase extra en el prompt lo clava.


Hay dos tipos de bugs. Los que lanzan errores — le pasas el stack trace a Claude y en 30 segundos tienes la respuesta. Los que no — el botón que no hace nada, la página que no se mueve, el formulario que falla en silencio — esos son los que Claude se equivoca al primer intento. No porque sea tonto. Porque no puede ver.

Me topé con tres de estos seguidos mientras armaba el flujo de pagos de how2claude. Aquí va el post-mortem, y los patrones de prompt que ahora uso para bugs silenciosos.

Bug 1: Un signo de interrogación de ancho completo escondido en una dirección de wallet

Conecté los pagos crypto de x402. En local funcionaba. Primer clic en producción: invalid_string at payTo en la consola. El flujo de firma ni siquiera arrancó — el schema de Zod del facilitator rechazó la petición antes.

La wallet era una dirección 0x... de 42 caracteres, a ojo se veía perfecta. Le pedí a Claude que revisara el campo wallet en config/credentials/production.yml.enc:

w = Rails.application.credentials.dig(:x402, :wallet_address).to_s
puts "length: #{w.length}"
# => 43

43 caracteres. Las direcciones EVM tienen 42. El carácter 43 era un signo de interrogación chino de ancho completo (U+FF1F) que entró al copiar-pegar desde un método de entrada en chino.

El primer escaneo de Claude no marcó nada — veía una cadena que empezaba con 0x y parecía correcta. No contó la longitud por iniciativa propia. Añade al prompt: "la dirección es un carácter más larga de lo esperado — imprime cada codepoint". Aparece 0xFF1F, caso cerrado.

Bug 2: El botón de Stripe Checkout no hacía nada al hacer clic

Botón Subscribe en la página de precios — haces clic, la página no se mueve. Sin errores. La pestaña network mostraba el POST saliendo, Stripe devolvía un 302 a checkout.stripe.com — y después... nada.

Primero hice que Claude revisara el controller. La lógica estaba bien: redirect_to session.url, allow_other_host: true. El JS — ningún listener relevante.

Al final noté la cabecera de respuesta: Content-Type: text/vnd.turbo-stream.html. Turbo estaba interceptando el submit de button_to como una petición Turbo Stream, y Turbo Stream no sigue los 302 cross-origin — así que el redirect se tragaba y la página se quedaba quieta en silencio.

Fix:

<%= button_to "Subscribe", ..., data: { turbo: false } %>

El mismo bug me volvió a pegar un mes después en el botón de Google OAuth. Los interceptores a nivel de framework son territorio fértil para bugs silenciosos — Claude por defecto razona linealmente request/response y no va a buscar una capa intermedia que reescribió la semántica. Añade al prompt: "recorre todos los interceptores a nivel de framework por los que pasa este clic — lista cada middleware/capa JS que procesa la petición en el camino browser → server → browser."

Bug 3: El toggle Mensual/Anual no hacía nada

El controller de Stimulus para el toggle mensual/anual en la página de precios — haces clic en el botón, no cambia nada. El método del controller se disparaba (confirmado con console.log), pero this.monthlyTarget era undefined.

Primer intento de Claude: error tipográfico en el nombre del target. No era. data-pricing-target="monthly" estaba en el DOM.

El problema era el scope. data-controller="pricing" estaba en el contenedor del botón del toggle, pero las dos secciones de grid quedaban fuera de ese contenedor. Stimulus solo busca targets dentro del subárbol del elemento controller; los de fuera no existen para él. Moví data-controller al <section> que envuelve todo — arreglado.

Este bug grita "el código está bien" — todos los nombres coinciden, todos los atributos están, la funcionalidad simplemente no funciona. Claude por defecto lee el código línea por línea; no va a visualizar la estructura del DOM por iniciativa propia. Añade al prompt: "dibuja el árbol de ancestros y descendientes del elemento con data-controller='pricing' — marca qué data-pricing-target caen dentro del subárbol y cuáles no."

Tres patrones de prompt para bugs silenciosos

Los tres bugs se veían idénticos por fuera: clic, no pasa nada, ningún error. Claude se equivocó cada vez, y cada vez una frase extra en el prompt lo dejó clavado. El patrón común:

1. Dile el delta cuantificado entre lo esperado y lo real — no solo "está mal"

No "la dirección de wallet tiene un problema", sino "es un carácter más larga de lo esperado".
No "el botón no funciona", sino "la respuesta es un 302, pero el browser no lo siguió".
No "el toggle está roto", sino "el método del controller se dispara, pero el target es undefined".

Cuanto más estrecho el delta, más pequeño el espacio de búsqueda de Claude.

2. Apúntalo a las capas invisibles — framework, browser, encoding

Los bugs silenciosos casi nunca viven en tu código de negocio. Viven en Turbo, en el scope de Stimulus, en la codificación de caracteres, en CSP, en CORS, en service workers. El default de Claude es leer tu código. Dile explícitamente que vaya a mirar esas otras capas.

3. Pídele estado intermedio, no conclusiones

"Imprime cada codepoint." "Lista las cabeceras de respuesta." "Vuelca el subárbol del DOM." Materializa el estado intermedio en lugar de pedirle a Claude que razone hasta la respuesta. La parte "silenciosa" de un bug silencioso es que un paso de la cadena de razonamiento tiene un supuesto oculto que no se cumple. Materializar el estado intermedio es cómo fuerzas que ese supuesto salga a la luz.


Los errores ponen a prueba lo que Claude sabe. Los bugs silenciosos ponen a prueba la calidad de la señal que le das. Cuanto más específico seas, más rápido encontrará la respuesta.