Three real "click does nothing" bugs — each one Claude missed until one extra sentence in the prompt locked it in.
Bugs come in two flavors. The ones that throw errors — hand Claude the stack trace and you'll have an answer in 30 seconds. The ones that don't — the button that does nothing, the page that won't move, the form that silently fails — those are the ones Claude gets wrong on the first try. Not because it's dumb. Because it can't see.
I hit three of these in a row while building out the payment flow on how2claude. Here's the post-mortem, and the prompt patterns I now use for silent bugs.
I wired up x402 crypto payments. Local worked. First production click: invalid_string at payTo in the console. The signing flow never even started — the facilitator's Zod schema rejected the request first.
The wallet was a 42-character 0x... address, looked fine to my eye. I asked Claude to check config/credentials/production.yml.enc:
w = Rails.application.credentials.dig(:x402, :wallet_address).to_s
puts "length: #{w.length}"
# => 43
43 characters. EVM addresses are 42. The 43rd character was a Chinese fullwidth question mark ? (U+FF1F) that came in during a copy-paste from a Chinese input method.
Claude's first scan didn't flag anything — to it, the string started with 0x and looked right. It didn't spontaneously count the length. Add this to the prompt: "the address is one character longer than expected — print each codepoint." 0xFF1F shows up, case closed.
Subscribe button on the pricing page — click it, page doesn't move. No errors. The network tab showed the POST going out, Stripe returning a 302 to checkout.stripe.com — and then... nothing.
I had Claude check the controller first. Logic was fine: redirect_to session.url, allow_other_host: true. JS — no relevant listener.
I finally noticed the response header: Content-Type: text/vnd.turbo-stream.html. Turbo was intercepting the button_to submission as a Turbo Stream request, and Turbo Stream doesn't follow cross-origin 302s — so the redirect got swallowed and the page silently stayed put.
Fix:
<%= button_to "Subscribe", ..., data: { turbo: false } %>
The same bug hit me again a month later on the Google OAuth button. Framework-level interceptors are prime territory for silent bugs — Claude defaults to linear request/response reasoning and won't go looking for a middle layer that rewrote the semantics. Add to prompt: "walk through every framework-level interceptor this click passes through — list every middleware/JS layer that processes the request from browser → server → browser."
The Stimulus controller for the monthly/yearly toggle on the pricing page — click the button, nothing switches. The controller method fired (confirmed with console.log), but this.monthlyTarget was undefined.
Claude's first guess: typo in the target name. Wasn't. data-pricing-target="monthly" was in the DOM.
The problem was scope. data-controller="pricing" was on the toggle-button container, but the two grid sections sat outside that container. Stimulus only looks for targets inside the controller element's subtree; the ones outside don't exist to it. Moved data-controller up to the wrapping <section> — fixed.
This bug screams "the code is right" — all names match, all attributes present, the feature is just broken. Claude defaults to reading the code line-by-line; it won't proactively visualize the DOM structure. Add to prompt: "draw the ancestor and descendant tree of the element with data-controller='pricing' — mark which data-pricing-target elements fall inside the subtree and which don't."
All three bugs looked identical from the outside: click, nothing happens, no error. Claude guessed wrong each time, and each time one extra sentence in the prompt locked it in. The common pattern:
1. Tell it the quantified delta between expected and actual — not just "it's wrong"
Not "the wallet address has an issue," but "it's one character longer than expected."
Not "the button doesn't work," but "the response is a 302, but the browser didn't follow it."
Not "the toggle is broken," but "the controller method fires, but the target is undefined."
The narrower the delta, the smaller Claude's search space.
2. Point it at the invisible layers — framework, browser, encoding
Silent bugs almost never live in your business code. They live in Turbo, in Stimulus scope, in character encoding, in CSP, in CORS, in service workers. Claude's default is to read your code. Tell it explicitly to go look at those other layers.
3. Ask for intermediate state, not conclusions
"Print each codepoint." "List the response headers." "Dump the DOM subtree." Materialize the intermediate state instead of asking Claude to reason its way to the answer. The "silent" part of a silent bug is that one step in the reasoning chain has a hidden assumption that doesn't hold. Materializing intermediate state is how you force that assumption out into the open.
Errors test what Claude knows. Silent bugs test the quality of the signal you give it. The more specific you are, the faster it finds the answer.