ある日、Claude Code に「この関数を3箇所まとめて整理してください」と頼んだら、1件目の Edit は通ったのに、2件目で String to replace not found in file、3件目で String to replace ... is not unique が立て続けに出て止まってしまいました。さっきまで同じファイルの同じ場所だったはずなのに、なぜ2件目から急に挙動が変わるのか — Claude Code を毎日触っているとぶつかる現象です。
アーティスト・クリエイターの廣川政樹です。2014年から個人でアプリ開発を続けており、累計5,000万ダウンロードを超える壁紙・癒し系アプリを運営しています。日々の作業はほぼ Claude Code に任せていて、SwiftUI のリファクタや MDX 記事の差し替えでこのエラー連鎖に何度もハマってきました。原因はだいたい2つに集約され、対処も毎回同じパターンで救えます。今日はその切り分け方と、私が実運用で採用している逃げ方を書き残しておきます。
何が起きているのか — Edit ツールは「前の Edit の結果」を見ない
最初に押さえておきたいのは、Claude Code の Edit ツールが「直前の Edit でファイルがどう変わったか」を内部で再Read していない、という事実です。複数の Edit を1ターン内で連続して投げると、Claude のモデルが頭の中に持っている「現在のファイル像」と、実際にディスク上のファイルがどんどんズレていきます。
具体的にはこういう動きになります。
- Claude が最初に
Readでファイル全体を読む - その内容をベースに、Edit 1件目・2件目・3件目の
old_stringをまとめて生成する - Edit 1件目が適用され、ファイル上の該当行が
new_stringに置き換わる - しかし Edit 2件目の
old_stringは、Read 時点のファイルを前提に作られている - もし1件目と2件目の対象テキストが重なっていれば、2件目の
old_stringはもう存在しない - もし1件目が
new_string側にも同じ文字列を含んでいれば、2件目のold_stringが複数箇所にマッチしてnot uniqueになる
つまり Edit は「直列に適用されるけれど、各 Edit の old_string は同じスナップショットから作られている」状態です。この性質を理解すると、後段の対処はだいぶ機械的に決まります。
ケース1: 1件目で消した文字列を2件目が探している
最頻ケースです。たとえば SwiftUI のビューに同じパターンの修飾子が並んでいて、1件目で .padding(.top, 16) を削除、2件目で .padding(.top, 16) を .padding(.top, 24) に変えるような流れを Claude が組み立てると、2件目の old_string は1件目を適用した直後のファイルにはもう残っていません。
Error: String to replace not found in file.
String: .padding(.top, 16)
私の AdMob 周りの SwiftUI 改修中によくぶつかります。広告ユニットを切り替えるとき、同じヘルパー関数の呼び出しが連続していて、Claude が「上を削って下も書き換えよう」と一気にやろうとして失敗するパターンです。
対処: Read を挟むよう Claude に明示する
私の手癖は、エラーが出たらまず一言「いま Read でファイルをもう一度開いてから、残りの修正だけ Edit してください」と指示することです。これだけで、Claude が改めてファイルの最新状態を取り直し、残った Edit 候補を作り直してくれます。
連続 Edit を最初から避けたい場合は、依頼の段階で次のように書き分けます。
- 一括で頼む書き方: 「3箇所まとめて修正してください」
- 安全に頼む書き方: 「上から順に1つずつ修正してください。各 Edit のあとに
Readで結果を確認してから次に進んでください」
後者にすると Claude は1件 Edit → 1件 Read → 次の Edit という流れを律儀に守ってくれます。やや遅くなりますが、巨大なリファクタや、構造を大きく変える作業ではこちらが安全です。
ケース2: 1件目が new_string 側に同じパターンを増やしてしまう
もう一つのよくあるパターンが、1件目の new_string の中にも同じ文字列が含まれていて、結果としてファイル全体での出現回数が増えてしまうケースです。
Error: String to replace is not unique in the file.
Multiple matches found for: const handleSubmit = async () =>
たとえば「const handleSubmit = async () => の中身を分割して、内部で同名の関数を呼び出す形にしてください」のような依頼で、1件目の Edit が分割後の呼び出し側にも同じシグネチャを残してしまうと、2件目以降の Edit がどの行を指しているのか分からなくなります。
対処: old_string を周囲ごと広げて一意化する
not unique が出たら、Claude に「old_string の前後3〜5行を含めて一意になるよう書き直してから再実行してください」と伝えると確実です。Edit ツールは部分一致ではなく完全一致で動くので、old_string の範囲を広げれば必ず特定できます。
私はファイル末尾の関数を修正するときは最初から } の閉じ括弧を含める、import 行を変えるときは前後の import 行も含める、というルールを Claude 側の CLAUDE.md に書いてしまっています。これだけで not unique の発生率がだいぶ下がります。
# CLAUDE.md より抜粋
Edit ツールを使うときは、`old_string` が必ず1箇所に絞れるように前後数行を含めてください。
特に import 行、return 文、空の中括弧の修正では一意化を最優先してください。ケース3: フォーマッタが裏で走って差分が変わっている
頻度はやや下がりますが、保存時自動整形と連続 Edit が組み合わさったときの厄介な不具合もあります。1件目の Edit が走った瞬間に VS Code の editor.formatOnSave 相当のフックや Prettier が動いてインデントが揃ってしまうと、2件目の old_string は元のインデントを前提にしているので一致しません。
これは Hook を PostToolUse で formatter を走らせている人がよく踏みます。私自身、SwiftFormat を Edit のたびに走らせていた時期にこの罠で時間を溶かしました。
対処: Edit の合間にフォーマッタを走らせない
私の現在の運用は、フォーマッタは「Edit の連続がひと段落したあと」にだけ走らせる構成です。具体的には、Hook の発火タイミングを PostToolUse の単発 Edit ごとではなく、Stop や SubagentStop のタイミングに寄せています。これで連続 Edit の最中に裏で差分が変わることを防げます。
{
"hooks": {
"Stop": [
{ "command": "swiftformat ." }
]
}
}フォーマッタを Edit ごとに走らせたい場合でも、その依頼の間だけ PostToolUse を一時的に外し、/hooks で再有効化する運用が安全です。
切り分けフロー — 私が実際に頭の中でやっている順序
エラーが出たときは、私はだいたいこの順番で考えています。
- エラーが
not foundかnot uniqueかを見る not foundなら、直前の Edit が同じ箇所を触ったかをログで確認するnot uniqueなら、直近のnew_stringがパターンを増やしていないかを目視で見る- どちらでもなさそうなら、
PostToolUseフックが裏で走っていないかを/hooksで確認する - 切り分けが終わったら、Claude に「
Readを入れ直してから残りを Edit して」とだけ伝える
ほとんどのケースは2〜3で原因が割れます。複数箇所を一気に編集する依頼を続けるうちに「Read を挟む粒度」が自然と身についてきて、最初から連鎖失敗を起こさない投げ方ができるようになります。
予防策 — 連鎖失敗を起こさないリファクタの分割
最後に、そもそも Edit 連鎖を起こさないための分割パターンを書いておきます。
1ファイルあたり Edit は3件までを上限にする: 3件を超えそうな修正は、Claude に「いったん区切って、1ファイルずつ Edit して Read で確認したあと次のファイルに進んでください」と頼みます。
構造を変える Edit と中身を変える Edit を分ける: 関数のシグネチャを変える Edit と、その中身を書き換える Edit を同じターンに混ぜると、old_string が消えやすくなります。「先にシグネチャだけ、確認後に中身」の2ターンに分けるのが安全です。
広範囲の置換は Bash + sed に逃がす: 同じ単語を10箇所以上で置換したい場合は、Edit より sed -i 's/old/new/g' のほうが確実です。Claude にも「同名置換が4箇所以上なら sed を使ってください」と CLAUDE.md に書いておくと、自然と賢く分岐してくれるようになります。
次のアクション
このエラーに遭遇したら、まずは「Read を挟んでから残りを Edit してください」とだけ Claude に伝えてみてください。それで7割は片付きます。残りの3割は old_string を周囲ごと広げて一意化するか、フォーマッタを一時停止するかで処理できます。
私自身まだ毎日この罠を踏みかけては救出する繰り返しですが、Edit 連鎖の仕組みを頭に入れておくだけで作業の流れが止まる時間はだいぶ短くなります。同じ症状に悩んでいる方の作業時間を少しでも縮められたら嬉しいです。お読みいただきありがとうございました。