text_editorのstr_replaceで複数マッチエラーを防ぐ実装
Anthropic APIのtext_editorツールは、str_replaceが複数箇所にマッチしたときにどう止めるかをアプリケーション側の実装に委ねています。事前カウントで0件・複数件を検出する実装と、エラー後の直し方をまとめます。
str_replaceで複数マッチが起きる理由
Anthropic-schemaのtext_editorツール(str_replace_based_edit_tool)は、view / str_replace / create / insert の4コマンドでファイルを操作します。str_replace は old_str に指定した文字列を new_str に置き換えるコマンドで、old_str は空白やインデントも含めて完全一致していなければなりません。
問題は old_str がファイル内に2箇所以上あるときです。公式ドキュメントは、アプリケーションが「一意のマッチを保証するか、適切なエラーメッセージを返す」よう求めています。ツールが複数マッチを検出して止めてくれるわけではなく、検出とエラー化はアプリケーション側の実装事項です。
実装しないまま置換処理を書くと、同じ文字列を持つ別の箇所まで書き換わります。Pythonの str.replace() は既定で全件を置換するため、1箇所だけ直すつもりの編集が複数箇所に及びます。ハンドラ全体の組み方はtext editor toolを自前実装する手順で扱っています。
事前カウントで0件・複数件を止める実装
対処は、置換の前に出現回数を数えて、1件でなければ実行せずエラーを返すことです。公式ドキュメントのPythonサンプルが次の形です。
def safe_replace(file_path, old_text, new_text):
"""Replace text only if there's exactly one match."""
with open(file_path, "r") as f:
content = f.read()
count = content.count(old_text)
if count == 0:
return "Error: No match found"
elif count > 1:
return f"Error: Found {count} matches"
else:
new_content = content.replace(old_text, new_text)
with open(file_path, "w") as f:
f.write(new_content)
return "Successfully replaced text"content.count(old_text) が0なら「見つからない」、2以上なら件数を返し、ちょうど1件のときだけ content.replace() を実行します。この関数を str_replace のハンドラに差し込めば、未マッチと複数マッチの両方を書き込み前に弾けます。集計の範囲は path で指定した1ファイルの中だけです。別ファイルに同名の関数があっても、複数マッチには数えられません。
手元で動かすと何が返るか
同じ形の関数を2つ持つ小さなファイル p.py を作り、上の safe_replace を4通りの old_text で呼びました(Python 3で実行)。
def is_even(n):
if n % 2 == 0:
return True
return False
def is_odd(n):
if n % 2 == 1:
return True
return False1 Error: Found 2 matches
2 Successfully replaced text
3 Error: No match found
4 Successfully replaced text1番目の old_text は return True の1行だけで、関数が2つあるので2件にマッチして止まります。2番目は if n % 2 == 0: と次の行をまとめて指定したため、ファイル内で1箇所にしか現れず通りました。3番目はインデントをタブにした \tif n % 2 == 1: で、見た目は同じ行でも文字列が一致せず0件です。4番目は def is_odd(n): から return True までの3行を指定して、後半の関数だけが書き換わりました。
ファイルの最終状態を見ると、書き換わったのは成功した2番目と4番目の箇所だけです。止まった1番目と3番目の呼び出しでは、ファイルに書き込みが一度も走っていません。エラーを返す分岐が open(file_path, "w") より前にあるためで、この順序が事前検証の要です。
止まる2つのエラー
0件のとき
old_str がファイルのどこにも一致していません。内容が違うだけでなく、タブとスペースの違いや行末の空白のずれでも起きます。見た目が同じ行を指定しているときは、空白文字を先に疑います。
複数件のとき
同じ文字列が2箇所以上にあります。件数はエラー文面に載せられるので、Claudeに衝突の規模を伝えられます。
エラーを返してから直るまでの往復
公式ドキュメントは tool_result として返すエラーの文面も例示しています。複数マッチの例は次のとおりです。
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "Error: Found 3 matches for replacement text. Please provide more context to make a unique match.",
"is_error": true
}is_error: true を立てて返すと、例外でアプリケーションが止まらず、会話が続いたままClaudeがやり直せます。ドキュメントのエラー例は4種類で、文面は次のとおりです。
| 場面 | content の文面 |
|---|---|
| ファイルがない | content の文面Error: File not found |
| 複数マッチ | content の文面Error: Found 3 matches for replacement text. Please provide more context to make a unique match. |
| 0件 | content の文面Error: No match found for replacement. Please check your text and try again. |
| 権限がない | content の文面Error: Permission denied. Cannot write to file. |
件数と次の一手(provide more context)を文面に入れておくと、Claudeは「どこまで広げるか」を判断する材料を得ます。成功時の例は Successfully replaced text at exactly one location. です。
複数マッチから置換成功までの流れ
- 1
Claudeが str_replace を呼ぶ
old_strとnew_strを載せたtool_useブロックが返ります。 - 2
アプリが件数を数える
件数が1以外なら、ファイルには触れません。
- 3
is_error 付きで返す
件数と文脈を足す指示を
tool_resultに入れます。 - 4
Claudeが old_str を長くして再送する
次の節の方法で広げた
old_strを送ります。
old_strをどこまで長くすれば一意になるか
return True のような1行だけの old_str は、同じ形の関数が複数あると衝突します。関数名を含む直前の行や、変数名を含む前後2〜3行までまとめると、1行ずつは重複していても塊としては一意になります。上の4番目の呼び出しが、この形です。
公式ドキュメントが str_replace の例に挙げているのは、primes.py の構文エラーを直す呼び出しです。old_str が for num in range(2, limit + 1)、new_str が末尾にコロンを足した形で、1行の修正になっています。こうした1行だけの old_str が通るのは、その行がファイルに1度しか現れないときに限られます。同じループ文が複数の関数にあるファイルでは、同じ呼び出しが複数マッチになります。
大きなファイルではmax_charactersが見える範囲を決める
text_editor_20250728 以降では、ツール定義に max_characters を指定して、view で大きなファイルを返すときの切り詰め文字数を決められます。公式の手順では、この切り詰めはアプリケーション側の view の実装として、ファイル内容を指定の長さにそろえて返す処理です。
safe_replace はファイル全体を数えるので、切り詰めの外にある重複も検出できます。一方でClaudeが old_str を組み立てる元になる view の結果は、切り詰められた範囲に限られます。範囲外に同じ文字列があると、Claudeには見えないまま複数マッチのエラーが返る形になります。
view には view_range(開始行と終了行の2つの整数、-1は末尾まで)も指定できます。編集したい付近の行を view_range で絞って確かめてから str_replace を呼ぶ運用なら、見えていない範囲の重複によるやり直しを減らせます。
undo_editの廃止とツールの版の違い
text_editorツールは、公式の変更履歴に4つの版が載っています。複数マッチ対策に関わるのは、2025年4月の undo_edit 削除です。
text_editorツールの版
- 2024年10月22日text_editor_20241022
Claude Sonnet 3.5向けの初版で、
view/create/str_replace/insert/undo_editの5コマンドを持ちます。このモデルは退役済みです。 - 2025年3月13日text_editor_20250124
Claude Sonnet 3.7向けとして記載された版で、機能は前の版と同じです。
- 2025年4月29日text_editor_20250429
Claude 4向けの版で、
undo_editが削除されました。ツール名はstr_replace_based_edit_toolになり、str_replaceを中心にした構成を名前に反映しています。 - 2025年7月28日text_editor_20250728
いくつかの問題を修正し、任意の
max_charactersを加えた版です。それ以外はtext_editor_20250429と同じとされています。
変更履歴に削除の理由は書かれていません。text_editor_20241022 と text_editor_20250124 には undo_edit があるため、この2つの版向けの実装例は「誤った置換は戻せる」前提で書かれています。text_editor_20250429 以降を使うなら、取り消しのコマンドが無いことを前提にします。
公式ドキュメントの注意書きは、重要なファイルの編集前にバックアップを取ることと、置換が1箇所にだけ一致するようにすることを、別々の項目として並べています。
def backup_file(file_path):
"""Create a backup of a file before editing."""
backup_path = f"{file_path}.backup"
if os.path.exists(file_path):
with open(file_path, "r") as src, open(backup_path, "w") as dst:
dst.write(src.read())safe_replace は実行前に防げる失敗(0件・複数件)を止め、backup_file は一意にマッチしたうえで内容が想定と違っていた場合の逃げ道になります。役割が違うので、取り消しコマンドの無い版では両方を置きます。
File Checkpointingは自前のハンドラを追跡しない
ここまでは、素のAnthropic APIでtext_editorツールのハンドラを自前で書く前提でした。Agent SDKでカスタムツールを組む場合も、この自前ハンドラは自分で書いたツールの一つにあたります。
Agent SDKには、ファイル変更を巻き戻すFile Checkpointingがあります。ただし追跡の対象は、Write・Edit・NotebookEditの3つの組み込みツールによる変更です。自分で実装した str_replace_based_edit_tool のハンドラが書き換えたファイルは、この仕組みの対象に入りません。Bashコマンド経由の変更も対象外です。
自前ハンドラで編集させる設計では、backup_file のような自前のバックアップか、gitによる履歴管理が復旧手段になります。Bashツールをアプリケーションに組み込む場合も、実行前の検証と実行後の復旧手段を組にする考え方は同じです。
複数マッチのやり直しはトークンにも響く
text_editorツールの定義は、通常の入出力トークンに加えて入力トークンを消費します。公式の料金節の表にあるのは text_editor_20250429 の700トークンです。
複数マッチのエラーが出るたびに、エラーを読み、より長い old_str を組み立て直し、str_replace を呼び直す往復が1回増えます。事前カウントで防げる失敗をエラー往復に任せると、その回数だけ余分な呼び出しが積み上がります。
まとめ
自前でハンドラを書くなら、書き込み前の件数チェックと編集前のバックアップを、最初から両方入れておく形が基本になります。