Free

Claude にサイレントバグをデバッグさせる

クリックしても何も起きない実バグ 3 つ。プロンプトに一文足すまで Claude は毎回外した。


バグには 2 種類ある。エラーを吐くバグは、スタックトレースを Claude に渡せば 30 秒で答えが返ってくる。エラーを吐かないバグ——ボタンを押しても何も起きない、ページが動かない、フォームが黙って失敗する——こういうバグは Claude も一発では当たらない。馬鹿だからじゃない。見えないからだ。

最近 how2claude の決済フローを組みながら、このタイプを 3 連続で踏んだ。ポストモーテムと、サイレントバグに使うようになったプロンプトパターンをまとめる。

Bug 1:ウォレットアドレスに紛れ込んだ全角クエスチョンマーク

x402 の暗号決済を繋いだ。ローカルは通った。本番で最初にクリックしたら invalid_string at payTo が console に出た。signing flow に入る前に、facilitator の Zod スキーマが弾いていた。

ウォレットは 0x... で 42 文字、目で見る限り問題なし。config/credentials/production.yml.enc の wallet フィールドを Claude に確認させた。

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

43 文字。EVM アドレスは 42 文字のはず。43 文字目は中国語の全角クエスチョンマーク (U+FF1F)。中国語 IME からコピペした時に紛れ込んでいた。

Claude の最初のスキャンでは異常を検出しなかった——0x で始まる文字列に見えていて、長さを自発的に数えなかった。プロンプトに一文追加する:「このアドレスは想定より 1 文字長い。コードポイントを 1 文字ずつ出力して」。0xFF1F が出た瞬間に原因確定。

Bug 2:Stripe Checkout ボタンが押しても無反応

Pricing ページの Subscribe ボタン、押してもページが動かない。エラーなし。network タブでは POST は出ていて、Stripe が checkout.stripe.com への 302 を返している——なのにページはそのまま。

まず Claude に controller を見させた。ロジックは正常:redirect_to session.url, allow_other_host: true。JS を見た——関連する listener なし。

散々眺めてから気付いた。レスポンスヘッダが Content-Type: text/vnd.turbo-stream.html。Turbo が button_to の送信を Turbo Stream リクエストとして掴んでおり、Turbo Stream はクロスオリジンの 302 を追わない——リダイレクトが飲まれて、ページが黙って留まっていた。

修正一行:

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

同じバグが 1 ヶ月後に Google OAuth ボタンでも再発した。フレームワークレベルのインターセプタはサイレントバグの温床だ——Claude のデフォルトは「リクエスト→レスポンス」の線形推論で、途中の層がセマンティクスを書き換えている可能性を自分から探りに行かない。プロンプトに追加:「このクリックが通過するフレームワークレベルのインターセプタを全部洗い出して。ブラウザ→サーバー→ブラウザの経路で、どの middleware/JS レイヤーがこのリクエストを処理しているかを列挙して」。

Bug 3:Monthly/Yearly トグルが押しても無反応

Pricing ページの月額/年額トグルの Stimulus controller、ボタンを押しても切り替わらない。Controller のメソッドは発火している(console.log で確認済み)が、this.monthlyTarget が undefined。

Claude の第一声:target 名のタイポ。違った。data-pricing-target="monthly" は DOM に存在する。

問題は scope だった。data-controller="pricing" がトグルボタンのコンテナに付いていて、monthly/yearly の grid はコンテナの外側にあった。Stimulus は controller 要素のサブツリー内でしか target を探さない。外側のは存在しないのと同じ。data-controller を section 全体を包む <section> に上げたら解決。

このバグは「コードは合ってる」と叫んでる感じがする——名前は全部合ってる、属性も全部ある、機能だけが壊れてる。Claude はコードを 1 行ずつ読むのがデフォルトで、DOM 構造を自分で可視化しにいかない。プロンプトに追加:「data-controller='pricing' が付いている要素の祖先と子孫を描いて、どの data-pricing-target がサブツリー内にあって、どれが外にあるかマークして」。

サイレントバグ向けの 3 つのプロンプトパターン

3 つのバグは外見は完全に同じだった:クリック、無反応、エラーなし。Claude は毎回最初外したけど、毎回プロンプトに一文足すだけで一発で当てた。共通パターン:

1. 「期待値 vs 実際」の 定量的な 差を伝える。「間違ってる」だけじゃダメ

「ウォレットアドレスがおかしい」ではなく「想定より 1 文字長い」。
「ボタンが効かない」ではなく「レスポンスは 302 だがブラウザが追従してない」。
「トグルが壊れてる」ではなく「controller のメソッドは発火するが target が undefined」。

差分が絞れているほど、Claude の探索空間が狭くなる。

2. 「見えない層」——フレームワーク、ブラウザ、文字コードに目を向けさせる

サイレントバグはほぼ業務コードに棲んでいない。Turbo にいる、Stimulus の scope にいる、文字エンコーディングにいる、CSP にいる、CORS にいる、service worker にいる。Claude のデフォルトは業務コードを読むこと。これらの層を見にいけと明示的に指示する。

3. 結論ではなく中間状態を出力させる

「コードポイントを 1 文字ずつ出して」「レスポンスヘッダを列挙して」「DOM サブツリーをダンプして」——Claude に推論させるのではなく、中間状態を実体化させる。サイレントバグの「サイレント」性は、推論チェーンのどこかに成立していない隠れた前提がある、というところに宿る。中間状態を実体化することで、その隠れた前提を強制的に表に出せる。


エラーが出るバグは Claude の知識量を試す。出ないバグは、こちらが渡す信号の質を試す。具体的であればあるほど、答えは早く見つかる。