Drei echte Bugs, bei denen der Klick nichts tut — Claude lag jedes Mal daneben, bis ein zusätzlicher Satz im Prompt alles festnagelte.
Bugs gibt es in zwei Geschmacksrichtungen. Die, die einen Fehler werfen — reich Claude den Stacktrace, und du hast in 30 Sekunden eine Antwort. Die, die keinen werfen — der Button, der nichts tut, die Seite, die sich nicht bewegt, das Formular, das still und leise scheitert — bei denen liegt Claude beim ersten Versuch falsch. Nicht weil er dumm ist. Weil er nicht sehen kann.
Drei solche Bugs bin ich hintereinander in den Payment-Flow von how2claude gestolpert. Hier ist das Post-Mortem und die Prompt-Muster, die ich inzwischen für stille Bugs verwende.
Ich habe die x402-Krypto-Zahlungen angebunden. Lokal lief alles. Erster Klick in Produktion: invalid_string at payTo in der Console. Der Signing-Flow ist nicht einmal gestartet — das Zod-Schema des Facilitators hat die Anfrage vorher abgelehnt.
Die Wallet war eine 42 Zeichen lange 0x...-Adresse, sah mit bloßem Auge einwandfrei aus. Ich ließ Claude das Wallet-Feld in config/credentials/production.yml.enc prüfen:
w = Rails.application.credentials.dig(:x402, :wallet_address).to_s
puts "length: #{w.length}"
# => 43
43 Zeichen. EVM-Adressen haben 42. Das 43. Zeichen war ein chinesisches Fullwidth-Fragezeichen ? (U+FF1F), das beim Copy-Paste aus einer chinesischen Eingabemethode reingerutscht war.
Claudes erster Scan schlug kein Alarm — für ihn war es ein String, der mit 0x beginnt und korrekt aussieht. Er hat die Länge nicht von sich aus gezählt. Nimm das in den Prompt auf: „Diese Adresse ist ein Zeichen länger als erwartet — gib jeden Codepoint einzeln aus." 0xFF1F taucht auf, Fall geschlossen.
Subscribe-Button auf der Pricing-Seite — Klick, Seite bewegt sich nicht. Keine Fehler. Der Network-Tab zeigte, dass das POST rausging und Stripe ein 302 auf checkout.stripe.com zurückgab — und dann... nichts.
Ich ließ Claude zuerst den Controller prüfen. Logik war in Ordnung: redirect_to session.url, allow_other_host: true. JS — kein relevanter Listener.
Schließlich fiel mir der Response-Header auf: Content-Type: text/vnd.turbo-stream.html. Turbo fing das Submit des button_to als Turbo-Stream-Request ab, und Turbo Stream folgt keinen cross-origin 302s — die Weiterleitung wurde verschluckt und die Seite blieb still stehen.
Fix:
<%= button_to "Subscribe", ..., data: { turbo: false } %>
Derselbe Bug erwischte mich einen Monat später beim Google-OAuth-Button. Interceptors auf Framework-Ebene sind Nährboden für stille Bugs — Claude argumentiert standardmäßig linear Request/Response und sucht nicht von sich aus nach einer mittleren Schicht, die die Semantik umgeschrieben hat. Ergänze den Prompt: „Gehe jeden Framework-Interceptor durch, den dieser Klick passiert — liste jeden Middleware/JS-Layer auf, der diese Anfrage auf dem Weg Browser → Server → Browser verarbeitet."
Der Stimulus-Controller für den monatlich/jährlich-Toggle auf der Pricing-Seite — Klick auf den Button, nichts schaltet um. Die Controller-Methode feuerte (mit console.log bestätigt), aber this.monthlyTarget war undefined.
Claudes erster Tipp: Tippfehler im Target-Namen. War es nicht. data-pricing-target="monthly" stand im DOM.
Das Problem war der Scope. data-controller="pricing" saß auf dem Container des Toggle-Buttons, aber die beiden Grid-Sections lagen außerhalb dieses Containers. Stimulus sucht Targets nur innerhalb des Subtrees des Controller-Elements; die außerhalb existieren für ihn nicht. data-controller auf das umschließende <section> hochgezogen — behoben.
Dieser Bug schreit „der Code ist richtig" — alle Namen stimmen, alle Attribute sind da, nur das Feature ist kaputt. Claude liest Code standardmäßig Zeile für Zeile; er visualisiert DOM-Strukturen nicht von sich aus. Ergänze den Prompt: „Zeichne den Vorfahren- und Nachfahren-Baum des Elements mit data-controller='pricing' — markiere, welche data-pricing-target innerhalb des Subtrees liegen und welche nicht."
Alle drei Bugs sahen von außen identisch aus: Klick, nichts passiert, kein Fehler. Claude lag jedes Mal zuerst daneben, und jedes Mal hat ein zusätzlicher Satz im Prompt ihn festgenagelt. Das gemeinsame Muster:
1. Nenne ihm das quantifizierte Delta zwischen Soll und Ist — nicht nur „es ist falsch"
Nicht „die Wallet-Adresse hat ein Problem", sondern „sie ist ein Zeichen länger als erwartet".
Nicht „der Button funktioniert nicht", sondern „die Response ist ein 302, aber der Browser ist ihr nicht gefolgt".
Nicht „der Toggle ist kaputt", sondern „die Controller-Methode feuert, aber das Target ist undefined".
Je enger das Delta, desto kleiner Claudes Suchraum.
2. Zeig ihm die unsichtbaren Schichten — Framework, Browser, Encoding
Stille Bugs leben fast nie in deinem Business-Code. Sie leben in Turbo, im Stimulus-Scope, im Character Encoding, in CSP, in CORS, in Service Workern. Claudes Default ist, deinen Code zu lesen. Sag ihm ausdrücklich, er soll sich diese anderen Schichten anschauen.
3. Frag nach Zwischenzuständen, nicht nach Schlussfolgerungen
„Gib jeden Codepoint aus." „Liste die Response-Header auf." „Dump den DOM-Subtree." Materialisiere Zwischenzustände, statt Claude zur Antwort räsonieren zu lassen. Das „still" an einem stillen Bug ist, dass irgendein Schritt in der Argumentationskette eine versteckte Annahme hat, die nicht hält. Zwischenzustände zu materialisieren ist, wie du diese Annahme zwingst, ans Licht zu kommen.
Fehler testen, was Claude weiß. Stille Bugs testen die Qualität des Signals, das du ihm gibst. Je spezifischer du bist, desto schneller findet er die Antwort.