Free

Depurando bugs silenciosos com Claude

Três bugs reais em que o clique não faz nada — Claude erra toda vez até uma frase extra no prompt prender o diagnóstico.


Bugs vêm em dois sabores. Os que lançam erros — entrega o stack trace para o Claude e em 30 segundos você tem a resposta. Os que não lançam — o botão que não faz nada, a página que não se move, o formulário que falha em silêncio — esses são os que o Claude erra na primeira tentativa. Não porque seja burro. Porque não consegue enxergar.

Peguei três desses em sequência enquanto montava o fluxo de pagamento do how2claude. Aqui vai o post-mortem, e os padrões de prompt que passei a usar para bugs silenciosos.

Bug 1: Um ponto de interrogação de largura total escondido num endereço de carteira

Plugei os pagamentos crypto do x402. Localmente rodou. Primeiro clique em produção: invalid_string at payTo no console. O fluxo de assinatura nem chegou a começar — o schema Zod do facilitator rejeitou antes.

A carteira era um endereço 0x... de 42 caracteres, a olho parecia impecável. Pedi pro Claude checar o campo wallet em config/credentials/production.yml.enc:

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

43 caracteres. Endereços EVM têm 42. O 43º caractere era um ponto de interrogação chinês de largura total (U+FF1F), que entrou no copia-cola vindo de um método de entrada em chinês.

A primeira varredura do Claude não sinalizou nada — pra ele era uma string começando com 0x e parecia certa. Não contou o comprimento por conta própria. Adicione isso ao prompt: "o endereço tem um caractere a mais do que o esperado — imprima cada codepoint". Aparece 0xFF1F, caso encerrado.

Bug 2: Botão do Stripe Checkout não fazia nada ao clicar

Botão Subscribe na página de preços — clica, a página não se move. Sem erros. A aba network mostrava o POST saindo, o Stripe retornando um 302 para checkout.stripe.com — e aí... nada.

Fiz o Claude checar primeiro o controller. A lógica estava correta: redirect_to session.url, allow_other_host: true. JS — nenhum listener relacionado.

Finalmente reparei no cabeçalho de resposta: Content-Type: text/vnd.turbo-stream.html. O Turbo estava interceptando o submit do button_to como uma requisição Turbo Stream, e o Turbo Stream não segue 302 cross-origin — o redirect era engolido e a página ficava silenciosamente parada.

Fix:

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

O mesmo bug voltou um mês depois no botão do Google OAuth. Interceptores de nível de framework são terreno fértil pra bugs silenciosos — o Claude por padrão raciocina linearmente request/response e não vai procurar uma camada intermediária que reescreveu a semântica. Adicione ao prompt: "percorra cada interceptor de nível de framework por onde esse clique passa — liste cada middleware/camada de JS que processa essa requisição no caminho browser → server → browser."

Bug 3: Toggle Mensal/Anual não fazia nada

O controller Stimulus do toggle mensal/anual na página de preços — clica no botão, nada muda. O método do controller disparava (confirmei com console.log), mas this.monthlyTarget era undefined.

Primeiro palpite do Claude: typo no nome do target. Não era. data-pricing-target="monthly" estava no DOM.

O problema era o scope. data-controller="pricing" estava no container do botão do toggle, mas as duas seções de grid ficavam fora desse container. O Stimulus só procura targets dentro da subárvore do elemento controller; os de fora não existem pra ele. Subi o data-controller pro <section> que envolve tudo — resolvido.

Esse bug grita "o código está certo" — todos os nomes batem, todos os atributos estão lá, só a funcionalidade está quebrada. Claude por padrão lê o código linha por linha; não vai visualizar a estrutura do DOM por conta própria. Adicione ao prompt: "desenhe a árvore de ancestrais e descendentes do elemento com data-controller='pricing' — marque quais data-pricing-target caem dentro da subárvore e quais não."

Três padrões de prompt para bugs silenciosos

Os três bugs pareciam idênticos por fora: clique, nada acontece, sem erro. Claude errou toda vez, e toda vez uma frase extra no prompt fechou a questão. Padrão comum:

1. Diga a ele o delta quantificado entre esperado e real — não só "está errado"

Não "o endereço da carteira tem um problema", mas "tem um caractere a mais do que o esperado".
Não "o botão não funciona", mas "a resposta é 302, mas o browser não seguiu".
Não "o toggle está quebrado", mas "o método do controller dispara, mas o target é undefined".

Quanto mais estreito o delta, menor o espaço de busca do Claude.

2. Aponte pras camadas invisíveis — framework, browser, encoding

Bugs silenciosos quase nunca moram no seu código de negócio. Moram no Turbo, no scope do Stimulus, na codificação de caracteres, no CSP, no CORS, em service workers. O padrão do Claude é ler o seu código. Diga explicitamente pra ele ir olhar essas outras camadas.

3. Peça estado intermediário, não conclusões

"Imprima cada codepoint." "Liste os headers de resposta." "Dê dump na subárvore do DOM." Materialize o estado intermediário em vez de pedir pro Claude raciocinar até a resposta. A parte "silenciosa" de um bug silencioso é que um passo da cadeia de raciocínio tem um pressuposto oculto que não se sustenta. Materializar o estado intermediário é como você força esse pressuposto a aparecer.


Erros testam o que o Claude sabe. Bugs silenciosos testam a qualidade do sinal que você dá a ele. Quanto mais específico, mais rápido ele acha a resposta.