Claude Codeでバグを潰す手順: 症状から原因をgit bisectで挟み撃ち
Claude Codeでデバッグする実践手順。スタックトレースの読ませ方、症状から仮説への絞り込み、git bisectとログ戦略、再現手順の渡し方、直ったをテストで固定するまで。
金曜の夕方、本番で500が出ました。エラーを丸ごとコピーしてClaude Codeに貼って、「直して」と一言。出てきた修正をそのまま入れたら、確かに500は消えた。
月曜、別の画面が静かに壊れていました。
あのとき僕がやったのは、デバッグじゃなくて「とりあえず黙らせた」だけでした。原因を確かめずにフタをすると、バグは隣の部屋に引っ越すだけなんですよね。エラーを貼って一発で直ることもあります。でも実務で効くのは、その運頼みを、毎回同じ手順に落とすことです。
この記事は、エラー辞書みたいな丸暗記の話ではありません。詰まった時に何をどの順でやるか、その手順をClaude Codeと回す話です。
この記事の要点
- Claude Codeに渡すのは「直して」ではなく、症状・再現手順・期待結果・直近の変更の4点セット。これだけで命中率が変わる。
- スタックトレースは上から読まない。自分のコードが最後に出てくる行を起点に、症状→仮説→検証を回す。
- 原因の場所が分からないときは挟み撃ち。
git bisectで「いつ壊れたか」を、コメントアウトで「どこが壊れたか」を二分探索する。 - ログは闇雲に増やさない。入力・分岐・出力の3点に絞り、調査が終わったら必ず消す。
- 「直った」は気分で判断しない。先に失敗するテストを書いて、それが緑になることで固定する。
「直して」ではなく事実を渡す
一番よくある失敗は、エラーメッセージだけを貼って「直して」と頼むことです。これだとClaude Codeは最短の修正に寄ります。最短が悪いわけではないけれど、原因を確かめないまま入れた修正は、次の変更であっさり戻ります。冒頭の僕がまさにこれでした。
代わりに、最初に事実を4点そろえて渡します。
| 渡すもの | 例 | これがないと起きること |
|---|---|---|
| 症状(エラー全文) | Cannot read properties of undefined (reading 'map') | 推測で見当違いの場所を直す |
| 再現手順 | 「ユーザー一覧APIが空のときに一覧画面を開く」 | 直ったか確認できない |
| 期待結果と実際 | 期待: 空表示/実際: 画面が落ちる | どこからがバグか曖昧なまま |
| 直近の変更 | 「昨日 users の型を optional にした」 | 容疑者を絞れない |
この4点をそろえると、Claude Codeは事実と推測を分けやすくなります。たとえば「APIレスポンスが undefined なのか、空配列なのか、配列だけど name が空なのか」で修正は全部違います。ここを最初に固めないと、何回直しても堂々巡りになります。
僕は依頼の冒頭に、必ずこのテンプレを貼るようにしています。
目的:
原因を特定し、回帰テストを追加し、最小修正で直す。
観察:
- エラー全文:
- 再現手順:
- 期待結果:
- 実際の結果:
- 直近で変更したファイル:
制約:
- 先に仮説を3つ出す
- 大きなリファクタをしない
- any で型エラーを隠さない
- 追加したログは最後に削除する
- npm test と npm run typecheck を最後に実行する
報告:
- 原因
- 変更ファイル
- 追加した回帰テスト
- 残るリスク
このテンプレを使うと、Claude Codeは修正係ではなく調査相手になります。レビューする側も、差分を読む前に「原因・証拠・残リスク」を受け取れます。チームのテスト設計を先にそろえたい人は Claude CodeでTDDを実践する方法 も合わせて読むと、この後の「テストで固定する」が楽になります。
スタックトレースは上から読まない
初心者だった頃の僕は、スタックトレースを一番上から順番に読んでいました。これがまず遠回りでした。
上のほうに出るのは、たいていライブラリやランタイムの内部です。あなたが直せる場所ではありません。見るべきは、自分のプロジェクトのファイルが最後に登場する行です。そこが「自分のコードが最後にバトンを渡した地点」、つまり調査の起点になります。
Claude Codeにスタックトレースを渡すときは、こう頼みます。
このスタックトレースのうち、自分のコード(src/ 配下)が
最後に出てくる行はどこか教えてください。
そこで何の値がどうなっていたら、このエラーになるかを3つ挙げてください。
まだ修正はしないでください。
「まだ修正しないで」が地味に効きます。これを言わないとClaude Codeは先回りして直し始めて、肝心の「何が起きていたか」の確認が飛びます。原因の仮説を3つ出させて、その中から「一番ありそうで、一番安く確かめられるもの」を僕が選ぶ。この一拍があるだけで、的外れな修正がぐっと減ります。
スタックトレースが長すぎて読みづらいときは、全文を貼った上で「自分のコードの行だけ抜き出して」と頼むと、ノイズが落ちて会話が早くなります。
症状→仮説→検証を小さく回す
デバッグの本体は、当てっこではなく外れた仮説を安く捨てる作業です。一発で原因を当てようとすると、たいてい外れて時間を溶かします。だから僕は、必ず小さいループにします。
- 症状を一文にする。「3月を選んだのに3月31日のデータが消える」のように、何が起きているかを具体的に書く。
- 仮説を3つ出す。Claude Codeに出させてもいい。「月末が範囲外」「タイムゾーンのずれ」「APIが31日を返していない」など。
- 一番安い検証から潰す。コードを直す前に、
console.log一行や小さいテストで確かめる。 - 外れたら仮説を捨てて2に戻る。当たったらやっと修正に進む。
具体例で見ます。実際の現場でよく出る「undefined の map で落ちる」やつです。クラッシュを消すだけなら users ?? [] で終わりですが、それだと「name が null のとき」「空文字のとき」の扱いが曖昧に残ります。症状を「APIのpayloadが欠けると一覧が落ちる。期待は空表示。空の表示名は出したくない」とまで言語化すると、修正の形が決まります。
export type User = {
id: string;
name?: string | null;
};
// payload が欠けても落ちず、使える表示名だけを返す
export function normalizeUsers(users: User[] | null | undefined): string[] {
if (!Array.isArray(users)) return [];
return users
.filter((user): user is User & { name: string } => typeof user.name === "string")
.map((user) => user.name.trim())
.filter((name) => name.length > 0);
}
ポイントは、users ?? [] で「クラッシュを黙らせる」のではなく、「欠けたpayloadは空リスト、空の表示名は除外」という仕様にしたことです。仕様にすると、次の章でそのままテストに落とせます。
どこで壊れたか分からない時の挟み撃ち
「エラーは出るけど、原因の場所が全然見当つかない」。これが一番つらい状況です。ここで力を出すのが二分探索、つまり挟み撃ちです。
いつ壊れたか: git bisect
「先週は動いてたのに、今日は壊れてる」。このパターンは、コードを読むより履歴を二分探索したほうが速いです。git bisect は「OKだったコミット」と「壊れてるコミット」を教えると、間を半分ずつ試して、バグを入れたコミットを特定してくれます。100コミットあっても7回くらいの判定で犯人にたどり着けます。
手動で「動く?」「動かない?」と答えてもいいですが、判定をテストで自動化すると一気に楽になります。下のスクリプトはそのまま動きます。yarn でも pnpm でも、テストコマンドだけ差し替えてください。
#!/usr/bin/env bash
# bisect-run.sh — どのコミットでテストが壊れ始めたかを自動で特定する
set -euo pipefail
GOOD_COMMIT="${1:?使い方: ./bisect-run.sh <動いていたコミット> [テストコマンド]}"
TEST_CMD="${2:-npm test}"
# 二分探索を開始: HEAD は壊れている / 指定コミットは動いていた
git bisect start
git bisect bad HEAD
git bisect good "$GOOD_COMMIT"
# 各コミットでテストを自動実行。
# 終了コード 0 を good、1〜124 を bad として git が犯人を絞り込む。
git bisect run bash -c "$TEST_CMD"
# 後片付け(元のブランチに戻す)
git bisect reset
echo "完了: 上のログの 'first bad commit' が原因コミットです"
使い方はこれだけです。
chmod +x bisect-run.sh
./bisect-run.sh a1b2c3d "npm test -- export-orders"
犯人コミットが分かったら、その差分をClaude Codeに渡します。「このコミットの差分が原因でテストXが壊れた。何が変わって落ちるようになったか説明して」と頼むと、調査範囲が1コミットに絞られているぶん、答えがとても具体的になります。git bisect の細かい使い方は Gitの公式ドキュメント が一次情報として確実です。
どこが壊れたか: コメントアウトで挟む
履歴ではなく、1つの関数の中で原因が分からないときは、コードのほうを二分探索します。怪しい処理の後半をまるごとコメントアウトして、エラーが消えるか見る。消えたら原因は後半、消えなければ前半。これを繰り返して範囲を半分ずつ狭めます。
Claude Codeにやらせるなら、「この関数の処理を前半・後半に分けて、後半を一時的に無効化したバージョンを出して。どちらに原因があるか切り分けたい」と頼みます。原始的に見えますが、再現が安定しているバグには一番速いことが多いです。
ログは3点に絞り、必ず消す
挟み撃ちで範囲を絞ったら、最後はその場所に値を吐かせて確かめます。ここで僕がよくやらかしたのが、ログを入れすぎることでした。10個もログを撒くと、出力の山に埋もれて逆に何も分からなくなります。
入れるのは原則この3点だけにします。
- 入力: その関数に何が渡ってきたか
- 分岐: どの
ifを通ったか(または通らなかったか) - 出力: 何を返した・投げたか
非同期のバグは「たまに動く」ように見えて厄介です。リトライ処理なら、何回呼ばれたか・待機したか・最後のエラーを投げ直したかをこの3点で押さえます。下は調査対象になりがちなリトライの実装です。
type RetryOptions = {
times: number;
delayMs: number;
sleep?: (ms: number) => Promise<void>;
};
const defaultSleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
export async function retry<T>(
task: () => Promise<T>,
{ times, delayMs, sleep = defaultSleep }: RetryOptions,
): Promise<T> {
let lastError: unknown;
for (let attempt = 1; attempt <= times; attempt += 1) {
try {
return await task();
} catch (error) {
lastError = error;
if (attempt < times) await sleep(delayMs);
}
}
// 最後のエラーを握りつぶさず、必ず投げ直す
throw lastError instanceof Error ? lastError : new Error("Retry failed");
}
ここで一番危ないのは、catch の中で最後のエラーを握りつぶすことです。握りつぶすと、本番では「なぜか失敗するけどログに何も残らない」最悪の状態になります。Claude Codeに調査を頼むときは「最終エラーを飲み込まないこと」「調査用ログは最後に削除すること」を毎回明記します。sleep を差し替え可能にしてあるのは、テストで待ち時間をゼロにして高速に再現するためです。
調査用のログを消し忘れて本番に流すと、それ自体が次の事故になります。構造化して残すべきログと、調査が終わったら消すべきログの線引きは Claude Codeの構造化ログ設計 で詳しく扱っています。
「直った」をテストで固定する
ここが、冒頭の僕が飛ばして痛い目を見た工程です。手元で動いた、エラーが消えた、で完了にすると、同じバグは平気で戻ってきます。直したら、そのバグを捕まえるテストを一本残す。これでバグは「二度と通れない関所」を一つ越えたことになります。
順番が大事で、先にそのバグで失敗するテストを書きます。テストが赤くなる(=バグを再現できている)ことを確認してから、修正して緑にする。先に書くことで「本当にその原因を直したのか」が証明できます。
さっきの normalizeUsers なら、こうなります。
import { describe, expect, it } from "vitest";
import { normalizeUsers } from "../src/normalize-users.js";
describe("normalizeUsers", () => {
it("payload が欠けたら空リストを返す", () => {
expect(normalizeUsers(undefined)).toEqual([]);
expect(normalizeUsers(null)).toEqual([]);
});
it("使える表示名だけを残す(空文字・null は除外)", () => {
expect(
normalizeUsers([
{ id: "u1", name: " Masa " },
{ id: "u2", name: "" },
{ id: "u3", name: null },
]),
).toEqual(["Masa"]);
});
});
Claude Codeへの頼み方も「先に失敗するVitestを追加し、その後に最小修正をして、npm test と npm run typecheck の結果を報告して」と順番を指定します。日付境界やタイムゾーンのバグも同じで、画面のスクリーンショットを渡すより、入力配列と期待するIDをテストにしたほうが調査精度が上がります。「3月を選んだのに31日が消える」なら、月末ぎりぎりの時刻を入れた配列を作って、期待するIDを書く。これで原因がUIなのか比較条件なのか、一発で切り分きます。
修正後の差分を人とAIで同じ基準でレビューしたいなら、この「原因・証拠・残リスク」をコミット前の関門に入れる build error triage loop のやり方が相性いいです。
よくある質問
Q. エラーを貼って「直して」だけでも直りますよ。手順なんて要りますか? 簡単なバグなら直ります。問題は「原因を確かめずに直る」ケースで、これは隣の機能を静かに壊しがちです。4点セット(症状・再現・期待結果・直近の変更)を渡すコストは数十秒。割に合います。
Q. git bisect は壊れたコミットが分からないと使えませんか? 「壊れているコミット(普通はHEAD)」と「動いていたコミット」の2つさえ分かれば使えます。後者は「先週リリースしたタグ」くらいの当たりで十分です。間はgitが二分探索で詰めてくれます。
Q. 再現手順がうまく言葉にできません。 まず「何をしたら/何が起きて/何を期待していたか」の3つを箇条書きにしてください。それでも曖昧なら、その曖昧さ自体をClaude Codeに渡して「再現手順を確定させる質問を3つして」と頼むと、ヒアリングしてくれます。
Q. ログとデバッガ、どちらを使うべきですか? 再現が安定していてステップ実行したいならデバッガ、本番やCIなど後から眺めたい場面ならログ、と使い分けます。Claude Codeと進めるなら、会話に貼れるぶんログのほうが共有が速いことが多いです。ただし調査用ログは消し忘れに注意。
Q. 「直った」と思ったのにまた再発します。 テストで固定していない可能性が高いです。再発したバグで失敗するテストを先に書き、赤を確認してから直す。これをやると、次に同じ原因で壊れた瞬間にCIが止めてくれます。
実際に試した結果
金曜の500事件のあと、僕はデバッグのやり方を一個だけ変えました。「直して」をやめて、症状・再現・期待・直近の変更の4点を必ず先に書く。それだけで、Claude Codeの一発命中がはっきり増えました。
一番効いたのは git bisect です。前は「どこだろう」とコードを延々眺めていた時間が、犯人コミットを7回の判定で出して、その差分だけをClaude Codeに渡す流れに変わりました。原因が1コミットに絞れていると、説明も修正も別物の精度になります。そして直すたびにテストを一本残すようにしたら、「前に直したはずのバグ」がCIで止まるようになって、月曜に静かに壊れている恐怖がなくなりました。
賢いAIに丸投げするより、詰まった時の手順を先に決めておく。遠回りに見えて、これが一番速い、というのが今の実感です。手元のデバッグ手順をチームの基準に育てたい人は 研修・相談 もどうぞ。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
制作会社がClaude Codeに触らせる前に決める権限チェックリスト
クライアントサイトを壊さずにAI編集を使うための、制作会社向け権限と確認の型です。
SaaSサポートのバグ報告をClaude Codeで再現手順に変える実務フロー
問い合わせ文をそのまま開発へ投げず、再現手順、証拠、次の一手に整えるサポート向け手順です。
Obsidianの古いメモをClaude Codeの指示書に変える10分ルーチン
Obsidianに溜めたメモが毎回ゴミになる人へ。事実・決定・未確認に仕分けして、Claude Codeがそのまま動ける指示書に変える朝の10分の型を紹介します。