Free

Debuggare i bug silenziosi con Claude

Tre bug reali in cui il clic non fa nulla — Claude sbaglia ogni volta finché una frase in più nel prompt inchioda la risposta.


I bug sono di due tipi. Quelli che lanciano un errore — passi lo stack trace a Claude e in 30 secondi hai la risposta. Quelli che non lo lanciano — il bottone che non fa niente, la pagina che non si muove, il form che fallisce in silenzio — quelli Claude li sbaglia al primo tentativo. Non perché sia stupido. Perché non riesce a vedere.

Me ne sono beccati tre di fila mentre costruivo il flusso di pagamento di how2claude. Ecco il post-mortem, e i pattern di prompt che adesso uso per i bug silenziosi.

Bug 1: Un punto interrogativo fullwidth nascosto in un indirizzo wallet

Ho collegato i pagamenti crypto x402. In locale funzionava. Primo click in produzione: invalid_string at payTo in console. Il signing flow non è nemmeno partito — lo schema Zod del facilitator ha respinto la richiesta prima.

Il wallet era un indirizzo 0x... da 42 caratteri, a occhio pulito. Ho fatto controllare a Claude il campo wallet in config/credentials/production.yml.enc:

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

43 caratteri. Gli indirizzi EVM ne hanno 42. Il 43° carattere era un punto interrogativo cinese fullwidth (U+FF1F) entrato durante un copia-incolla da un metodo di input cinese.

La prima scansione di Claude non aveva segnalato nulla — per lui era una stringa che iniziava con 0x e sembrava corretta. Non ha contato la lunghezza di sua iniziativa. Aggiungi al prompt: "questo indirizzo è un carattere più lungo del previsto — stampa ogni codepoint". Spunta 0xFF1F, caso chiuso.

Bug 2: Il bottone Stripe Checkout non reagiva al click

Bottone Subscribe nella pagina pricing — click, la pagina non si muove. Nessun errore. Il tab network mostrava il POST in uscita, Stripe che restituiva un 302 verso checkout.stripe.com — e poi... niente.

Ho fatto controllare prima il controller a Claude. Logica ok: redirect_to session.url, allow_other_host: true. Il JS — nessun listener rilevante.

Alla fine ho notato l'header di risposta: Content-Type: text/vnd.turbo-stream.html. Turbo stava intercettando il submit di button_to come richiesta Turbo Stream, e Turbo Stream non segue i 302 cross-origin — quindi il redirect veniva ingoiato e la pagina rimaneva silenziosamente ferma.

Fix:

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

Lo stesso bug mi è tornato addosso un mese dopo sul bottone Google OAuth. Gli interceptor a livello di framework sono terreno fertile per i bug silenziosi — Claude di default ragiona linearmente richiesta/risposta e non va a cercare uno strato intermedio che abbia riscritto la semantica. Aggiungi al prompt: "percorri ogni interceptor a livello di framework attraversato da questo click — elenca ogni middleware/layer JS che processa questa richiesta lungo il percorso browser → server → browser."

Bug 3: Il toggle Monthly/Yearly non reagiva

Lo Stimulus controller per il toggle mensile/annuale nella pagina pricing — click sul bottone, niente si alterna. Il metodo del controller si attivava (confermato con console.log), ma this.monthlyTarget era undefined.

Prima ipotesi di Claude: typo nel nome del target. Non lo era. data-pricing-target="monthly" era nel DOM.

Il problema era lo scope. data-controller="pricing" era sul container del bottone toggle, ma le due sezioni grid stavano fuori da quel container. Stimulus cerca i target solo nel sottoalbero dell'elemento controller; quelli esterni non esistono per lui. Ho spostato data-controller sul <section> che avvolge tutto — risolto.

Questo bug urla "il codice è giusto" — tutti i nomi combaciano, tutti gli attributi sono lì, è solo la feature a essere rotta. Claude di default legge il codice riga per riga; non va a visualizzare la struttura del DOM di sua iniziativa. Aggiungi al prompt: "disegna l'albero di antenati e discendenti dell'elemento con data-controller='pricing' — segna quali data-pricing-target cadono dentro il sottoalbero e quali no."

Tre pattern di prompt per i bug silenziosi

I tre bug sembravano identici dall'esterno: click, non succede niente, nessun errore. Claude ha sbagliato la prima mossa ogni volta, e ogni volta una frase in più nel prompt l'ha inchiodato. Il pattern comune:

1. Digli il delta quantificato fra atteso e reale — non solo "è sbagliato"

Non "l'indirizzo wallet ha un problema", ma "è un carattere più lungo del previsto".
Non "il bottone non funziona", ma "la risposta è un 302, ma il browser non l'ha seguita".
Non "il toggle è rotto", ma "il metodo del controller si attiva, ma il target è undefined".

Più stretto è il delta, più piccolo è lo spazio di ricerca di Claude.

2. Indirizzalo verso i layer invisibili — framework, browser, encoding

I bug silenziosi non vivono quasi mai nel tuo codice di business. Vivono in Turbo, nello scope di Stimulus, nella codifica dei caratteri, in CSP, in CORS, nei service worker. Il default di Claude è leggere il tuo codice. Digli esplicitamente di andare a guardare quei layer.

3. Chiedigli lo stato intermedio, non le conclusioni

"Stampa ogni codepoint." "Elenca gli header di risposta." "Fai il dump del sottoalbero DOM." Materializza lo stato intermedio invece di chiedere a Claude di ragionare fino alla risposta. Il "silenzioso" in un bug silenzioso è che un passaggio della catena di ragionamento ha un'assunzione nascosta che non regge. Materializzare lo stato intermedio è il modo in cui costringi quell'assunzione a venire allo scoperto.


Gli errori mettono alla prova ciò che Claude sa. I bug silenziosi mettono alla prova la qualità del segnale che gli dai. Più sei specifico, più in fretta trova la risposta.