AIに丸ごとリファクタは事故る。Claude Codeで安全に直す段取り
Claude Codeでリファクタリングを安全に進める段取り。テストで挙動を固定→小さく刻む→git diffで確認。広範囲の一括置換で僕がやらかした事故と回避策つき。
「このファイル、ついでに全部きれいにしといて」
軽い気持ちでそう頼んだら、Claude Codeは本当に全部きれいにしてくれました。関数を分割し、変数名を直し、ついでにフォーマットも整え、150行の差分が返ってきた。見た目は完璧。でもテストを回したら、送料無料の判定が >= から > に変わっていて、9999円の注文が無料配送から外れていました。
幸いマージ前に気づいたから良かったものの、あれが本番に出ていたらと思うとゾッとします。
リファクタリングは「動くものを、動くまま、読みやすくする」作業です。なのにAIに大きく任せるほど、こっそり挙動が変わる。賢いから危ないんですね。今日は、僕が何度かやらかして身につけた「事故らない段取り」を、コピペで動くコードと一緒に書きます。
この記事の要点
- リファクタリングの安全装置は「先にテストで今の挙動を固定する」こと。直すのはその後。
- Claude Codeには一度に1テーマだけ。1差分3ファイル以内に刻むとレビューが追いつく。
- AIに任せるのは「機械的な書き換え」、人が見るのは「仕様が変わってないか」。役割を分ける。
- 最終判断は会話の説明ではなく
git diffで下す。説明と差分が食い違ったら差分を信じる。 - 一番多い事故は「広範囲の一括置換」。リネームもフォーマットも、混ぜずに分けて出させる。
まず「直す前」にやることが9割
リファクタリングで失敗する人は、だいたい順番を間違えています。いきなり「賢く直して」から始める。本当に最初にやるのは、直すことじゃなくて今の挙動を逃げられないように固定することです。
レガシーな関数って、たいてい「なんでこうなってるのか分からないけど、消すと誰かが困る」分岐を抱えています。テストがその挙動を捕まえていれば、AIがうっかり変えた瞬間に赤くなる。テストがなければ、変わったことにすら気づけません。冒頭の送料事故も、テストがあったから手前で止まりました。
だから僕は、Claude Codeに編集を許す前に、必ず2段階で頼みます。
- 調査だけを頼む(まだ1文字も編集させない)
- 3ステップ以内の計画を出させて、仕様変更が混ざっていないか人が読む
最初のプロンプトはこれで十分です。「まだ編集しないで」を明記するのがポイントで、これを書かないとClaude Codeは親切心で実装まで走ります。
このリポジトリで安全にリファクタリングできる候補を調査してください。
まだファイルは編集しないでください。
条件:
- 外部から見える挙動は変えない
- 1回の差分は3ファイル以内に収まる範囲
- 既存テストで確認できる箇所を優先
- 変更候補、理由、確認コマンド、想定リスクを表で出す
調査と実装を分けるだけで、事故率は体感で半分以下になります。「全部見てから一気にやる」より、「狭く見て、小さく直して、確かめる」を繰り返すほうが、結局は速い。急がば回れが、ここでは本当に効きます。
作業ブランチも先に切って、ベースラインを取っておきます。
git switch -c refactor/extract-order-total
git status --short
npm test
npm run typecheck
test や typecheck のコマンド名はプロジェクトごとに違うので、package.json の scripts を見て読み替えてください。ここで大事なのは、直す前に一度テストを通しておくこと。もし最初から赤いテストがあると、「元から落ちてたのか、自分が壊したのか」が分からなくなって、レビューが地獄になります。
挙動を固定する「特性評価テスト」を先に書く
テストがない関数を触るとき、いきなり仕様を勉強し直す必要はありません。今の出力をそのまま正解として記録するテストを書けば足ります。これを特性評価テスト(characterization test)と呼びます。「正しい挙動」ではなく「現在の挙動」を写し取るのが目的です。
例えば、こんな読みづらい注文計算があったとします。
// before: src/domain/order.ts
export function calc(o: { items: { p: number; q: number }[]; d?: number }) {
let t = 0;
for (const i of o.items) {
t += i.p * i.q;
}
if (o.d) {
t = t - o.d;
}
return Math.max(t, 0);
}
p q d が何なのか、書いた本人以外には分かりません。でも直す前に、まず今の答えを固定します。
// src/domain/order.test.ts
import { describe, expect, it } from "vitest";
import { calc } from "./order";
describe("calc の現在の挙動を固定する", () => {
it("単価 x 数量を合算する", () => {
// 1200円 x 2個 = 2400円。これが今の答え
expect(calc({ items: [{ p: 1200, q: 2 }] })).toBe(2400);
});
it("割引後にマイナスへ振れない", () => {
// 500円から800円引いても 0 で止まる、という今の仕様を記録
expect(calc({ items: [{ p: 500, q: 1 }], d: 800 })).toBe(0);
});
});
これは npm test -- order でそのまま走ります。緑になったのを確認してから、初めてClaude Codeに名前と型の改善を頼みます。
src/domain/order.ts の calc 関数をリファクタリングしてください。
条件:
- src/domain/order.test.ts が緑のまま通ること
- 外部公開名 calc は変えない
- 変数名と型名を、読んで意味が分かる名前にする
- 割引後にマイナスへ振れない挙動は維持
- 変更後に npm test -- order を実行して結果を報告
期待するAfterはこのくらいです。挙動は1ミリも変えず、読みやすさだけ上げます。
// after: src/domain/order.ts
type OrderLine = {
price: number;
quantity: number;
};
type OrderInput = {
items: OrderLine[];
discount?: number;
};
export function calc(order: OrderInput): number {
const subtotal = order.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0,
);
return Math.max(subtotal - (order.discount ?? 0), 0);
}
先にテストがあるおかげで、Claude Codeが万が一 Math.max を外したり、足し算を引き算に変えたりすれば、その場で赤くなる。安全網があると、AIに任せる勇気が出ます。テストの優先順位の付け方そのものはClaude Codeで決めるテスト戦略に詳しく書いたので、どこから書くか迷う人はそちらも見てください。
小さく刻む:1差分1テーマを守る
リファクタリングは、刻む大きさがすべてです。僕の失敗の9割は「一度に欲張った」ことから来ています。
副作用のある関数を直すときが、いちばん刻みがいが出ます。例えばDB保存とメール送信を含む注文処理。
// before: src/services/order-service.ts
export async function createOrder(input: CreateOrderInput) {
if (input.items.length === 0) {
throw new Error("items required");
}
const subtotal = input.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const shippingFee = subtotal >= 10000 ? 0 : 800;
const total = subtotal + shippingFee;
const order = await db.order.create({
data: { userId: input.userId, subtotal, shippingFee, total },
});
await mailer.sendOrderCreated(order.id);
return order;
}
ここで「読みやすくして」と丸投げすると、計算の抽出・命名・例外設計・フォーマットが全部混ざって返ってきます。冒頭の >= 事故は、まさにこれで起きました。だからやることと、やらないことの両方を指定します。
src/services/order-service.ts の createOrder を小さくしてください。
今回やること:
- 送料と合計金額の計算だけを副作用のない関数として抽出
- 関数名は calculateOrderTotals にする
- calculateOrderTotals の単体テストを追加
今回やらないこと:
- DB保存とメール送信の順序の変更
- 送料無料の境界 (>= 10000) の変更
- エラー文言の変更
- ファイル全体のフォーマット変更
Afterはこうなります。注目してほしいのは、createOrder の処理の流れ自体は変わっていないこと。計算ロジックを外に出しただけです。
// after: src/services/order-service.ts
export function calculateOrderTotals(items: OrderItem[]) {
const subtotal = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0,
);
// 1万円以上で送料無料。この境界は今回いじらない
const shippingFee = subtotal >= 10000 ? 0 : 800;
return { subtotal, shippingFee, total: subtotal + shippingFee };
}
export async function createOrder(input: CreateOrderInput) {
if (input.items.length === 0) {
throw new Error("items required");
}
const { subtotal, shippingFee, total } = calculateOrderTotals(input.items);
const order = await db.order.create({
data: { userId: input.userId, subtotal, shippingFee, total },
});
await mailer.sendOrderCreated(order.id);
return order;
}
この差分なら、レビューで見る場所は3つだけ。計算式・送料無料の境界・副作用の順番。10分で読み切れます。もし返ってきた差分が大きすぎたら、遠慮なく差し戻します。
今回の差分は大きすぎます。
フォーマットだけの変更を元に戻し、calculateOrderTotals の抽出とテスト追加だけに絞ってください。
DB保存とメール送信の順序、送料無料の境界、エラー文言は変えないでください。
「小さく刻む」は理想論に聞こえますが、実利です。差分が小さいほど、AIが事故っても被害が小さく、人が気づきやすい。
AIに任せる範囲と、人が必ず見る範囲
何を任せて、何を自分で見るか。ここの線引きがあいまいだと、ずっと不安なままです。僕は経験的に、こう分けています。
| 作業 | Claude Codeに任せる | 人が必ず見る |
|---|---|---|
| 変数・関数のリネーム | ◎ ほぼ任せきり | 公開APIの名前だけ確認 |
| 純粋関数の抽出 | ◎ テストがあれば安心 | 抽出元の流れが同じか |
any を具体型に | ○ 境界から1つずつ | 無理なキャストで黙らせてないか |
| 巨大関数の分割 | △ 計算部分だけ任せる | 分岐の順番・条件が同じか |
| 削除・課金・権限まわり | × 提案は聞くが実行は自分 | 業務影響を全部 |
判断の軸はシンプルで、間違えたときに機械が気づけるかです。リネームや純粋関数の抽出は、テストと型チェックが守ってくれる。一方、削除・課金・通知・権限は、テストが緑でも業務的に大事故になりうる。だからこの領域は、AIの実行権限を最初から渡しません。
実行権限を絞る具体的なやり方はClaude Codeの権限設定ガイドにまとめてあります。最初はテスト・型チェック・lint・読み取り系コマンドだけ許可して、安全だと分かった操作を後から足していくのがおすすめです。
any の削減も、境界から1つずつが鉄則です。外部からデータが入ってくる場所(APIレスポンス、フォーム、設定ファイル)から固めると、波及が読めます。
// after: src/lib/user-api.ts
export type UserResponse = {
id: string;
name: string;
profile?: { displayName?: string };
};
export async function fetchUser(id: string): Promise<UserResponse> {
const response = await fetch(`/api/users/${id}`);
return response.json() as Promise<UserResponse>;
}
export function getDisplayName(user: UserResponse): string {
// profile が無くても name に落ちる。落ちないことをテストで固定する
return user.profile?.displayName ?? user.name;
}
この as Promise<UserResponse> で十分かはプロジェクト次第です。実行時にも検証したいならzodなどを足す手もありますが、それは次の差分でやります。1差分にライブラリ追加まで詰め込むと、レビューが一気に重くなるからです。
git diff で見る:説明より差分を信じる
Claude Codeのリファクタリングは、最後は必ず git diff で判断します。会話の説明がどれだけ自然でも、差分が読めなければマージしてはいけません。「型エラー消えました」の一言の裏で、戻り値を空文字にしていた、なんてことが普通に起きます。
差分を読むときのコマンドはこのあたり。
git diff --check
git diff --stat
git diff --word-diff -- src/domain/order.ts
git diff --check は行末の空白や コンフリクトマーカーを検出します。--stat で変更量がひと目で分かる。--word-diff は単語単位で差分を出すので、>= が > に化けたような1文字の変化を見逃しません。冒頭の事故、まさにこれで見つけられる類です。
僕が毎回チェックする観点を表にしておきます。
| 観点 | 見るポイント |
|---|---|
| 仕様 | 入出力・例外・境界値・DB更新順が変わってないか |
| 差分量 | 1回で読み切れるファイル数か |
| テスト | 変更前の挙動を固定するテストがあるか |
| 型 | as any や無理なキャストで黙らせてないか |
| 副作用 | API・メール・課金・削除の順番が変わってないか |
| 一致 | Claude Codeの要約と実際の差分が食い違ってないか |
レビュー自体をClaude Codeに一次対応させるのも有効です。レビュー用のプロンプトを固定しておくと、毎回ブレません。
このgit diffをレビューしてください。
観点:
- リファクタリングの範囲を超えて仕様変更していないか
- テストで守られていない挙動はどこか
- 型を無理に黙らせていないか
- 追加で人間が見るべきファイルはどれか
出力:「問題なし / 要確認 / 修正必須」の3分類で、根拠とファイル名を添えてください。
ただし一次レビューをAIに任せても、削除・課金・通知・権限のコードは最後に必ず自分の目を通します。レビューのコメントの書き方やチームでの回し方はClaude Codeに一次レビューを任せる型にまとめたので、レビュー文化ごと整えたい人はそちらへ。
よくある事故と、その避け方
ここからは、僕が実際に踏んだ地雷です。
事故1: 広範囲の一括置換。これが断トツで多い。「getData を全部 fetchData にリネームして」と頼むと、Claude Codeは文字列一致で関係ない別モジュールの getData まで巻き込むことがあります。避け方は、まず「どこを変えるか一覧だけ出して」と置換対象を確認してから実行すること。IDEのシンボル単位リネームのほうが安全な場面も多いです。
事故2: テストなしで「差分がきれいだからOK」。見た目が読みやすくなっても、境界条件が変わっていることがあります。割引が0未満にならない、送料が一定額で無料になる、権限がないユーザーは弾く。こうした仕様は、必ずテストに落としてから触ります。
事故3: フォーマッタとリファクタを同じ差分に入れる。PrettierやESLintの自動修正が数百行の差分を作ると、本質的な変更が埋もれます。フォーマットだけのコミットと、構造変更のコミットは分ける。これだけでレビュー速度が段違いです。
事故4: テストがないコードに、いきなり構造変更。安全網がないまま大手術をすると、何が壊れたか永遠に分かりません。テストがないレガシーは、まず特性評価テストで現状を固定してから、少しずつ置き換える。この進め方はレガシーコードをClaude Codeで段階置き換えする手順に動くコードつきで書きました。
毎回これを貼ってから作業すると安定します。
## リファクタリング前チェック
- [ ] 変更目的は1つだけか
- [ ] 編集前に既存テストを通したか
- [ ] Before/Afterで外部から見える挙動は同じか
- [ ] 振る舞いを固定するテストがあるか
- [ ] git diff --stat が読み切れる量か
- [ ] any・無理なキャスト・例外握りつぶしが増えてないか
- [ ] DB・メール・課金・削除・権限の順序が変わってないか
Claude Codeの基本的な作業フローは公式のcommon workflowsが、権限とコマンド制御はsettingsが参考になります。
よくある質問
Q. テストが1本もないコードは、どこから手をつければいい? A. まず特性評価テストです。「正しい挙動」を考える前に、今の出力をそのまま記録するテストを書いて緑にする。それから直し始めれば、変えたつもりのない箇所が変わった瞬間に気づけます。
Q. Claude Codeにテストごと書かせていい?それとも自分で書く? A. テストの「叩き台」は任せていいですが、何を守るかは自分で決めます。AIに書かせたテストは、わざと1か所コードを壊して赤くなるか確認すると安心です。赤くならないテストは、何も守っていません。
Q. 1差分は何ファイルくらいが目安? A. 僕は3ファイル以内を目安にしています。本体・テスト・型定義くらい。それを超えると、レビューで集中力が切れて見落とします。大きくなりそうなら、テーマを分けて複数の差分にします。
Q. リネームを安全にやるには? A. 文字列一括置換ではなく、IDEのシンボル単位リネームを優先します。Claude Codeに頼むときも、まず「変更対象のファイルと箇所を一覧で出して」と確認してから実行すると、無関係なモジュールへの巻き込みを防げます。
Q. Claude Codeの説明とgit diffが食い違ったら?
A. 差分を信じてください。説明はあくまで自己申告です。git diff --word-diff で1文字単位まで確認し、不安な行はテストを足してから受け入れる。これで防げる事故がほとんどです。
実際に試した結果
この段取りで自分のリポジトリをしばらく回してみて、いちばん効いたのは「編集前の調査プロンプト」と「git diffのレビュー固定化」でした。Claude Codeは、対象が狭く・テストコマンドが明確で・やらないことが書いてあるほど、驚くほど素直に動きます。逆に「いい感じにして」だけだと、毎回差分が膨らんでレビューに時間を吸われました。
冒頭の送料事故以来、僕はリファクタリングのとき「AIを信用するか」で悩むのをやめました。代わりに見るのは、どのテストが守ってくれているかです。先にテストで挙動を固定し、小さく刻んで、git diff で確かめる。この3つを守るだけで、リファクタリングは「怖い作業」から「淡々とした作業」に変わります。
まずは、変数のリネームか純粋関数の抽出か any 削減の、どれか1つから試してみてください。型ができてきたら、Claude Codeに一次レビューを任せる型と組み合わせると、チームでも同じ品質で回せます。社内のルールごと整えたいなら、Claude Code研修で権限設計からレビュー観点まで一緒に組み立てられます。速さより先に、壊さず続けられる型を作る。そこができると、日々の改善が怖くなくなります。
無料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分の型を紹介します。