text editor toolを自前実装する手順 — max_charactersとundo_edit廃止
text_editor_20250728をスキーマ不要のツールとして宣言し、view・create・str_replace・insertを自前で処理する手順です。max_charactersの扱いとundo_edit廃止の履歴も載せます。
text editor toolは何を自前で実装するのか
text editor toolは、Claudeにファイルの閲覧と編集をさせるためのAnthropic定義のツールです。ツール名は str_replace_based_edit_tool で、タイプは text_editor_20250728 を指定します。Claude 4以降のモデル向けのタイプです。
このツールはクライアントツールです。Claudeが tool_use ブロックで「このファイルを開く」「この文字列を置換する」と要求するだけで、ファイルには触れません。実際の読み書きは、呼び出し側のコードが担います。
スキーマ不要のツール(schema-less tool)という点が、普通のツール定義との違いです。input_schema を書く必要はなく、スキーマはモデルに組み込まれていて変更できません。自前で用意するのは次の2つだけです。
- リクエストに載せるツール宣言(タイプ・名前・任意の
max_characters) tool_useの入力を受けて、ファイルを操作して結果を返すハンドラ
同じクライアントツールの実装でも、シェル実行はBashツールをAPIで実装するセンチネル行とタイムアウト処理、画面操作はComputer Useツールを自前実装する最小構成にまとめています。本記事はファイル編集の側です。
ツール宣言とmax_characters
宣言は次の形です。max_characters は省略できます。
{
"type": "text_editor_20250728",
"name": "str_replace_based_edit_tool",
"max_characters": 10000
}max_characters は、大きなファイルを view で開いたときの切り詰めを制御するパラメータです。text_editor_20250728 以降のバージョンでしか使えません。古いバージョンのタイプに付けても効きません。
注意したいのは、切り詰めを実行するのが実装側だという点です。view を処理する手順には「ツール設定に max_characters を指定していたら、ファイルの内容をその長さに切り詰める」と書かれています。宣言に数字を書いただけでは何も起きません。ハンドラが自分で text[:max_characters] のように削り、削った結果を tool_result に入れます。
切り詰めた末尾に目印を付けるかどうかは、仕様に規定がありません。目印を付けるなら、Claudeが「続きがある」と判断できる文言と、view_range で続きを読めることを伝える一文が役に立ちます。この点は設計の判断で、仕様の指定ではありません。
追加で消費される入力トークン
ツールを宣言すると、リクエストごとに固定の入力トークンが加算されます。
| 対象モデル | 追加の入力トークン |
|---|---|
| Claude 4.7以降のモデルとClaude Mythos Preview | 追加の入力トークン974 |
| Claude 4.6以前のモデル | 追加の入力トークン745 |
料金体系は他のツールと同じで、通常の入力・出力トークン単価に従います。
4つのコマンドと入力パラメータ
text_editor_20250728 が受け付けるコマンドは view / str_replace / create / insert の4つです。ハンドラは input.command で振り分けます。
| コマンド | 主なパラメータ | 役割 |
|---|---|---|
view | 主なパラメータpath、任意で view_range | 役割ファイルの内容、またはディレクトリの一覧を返す |
str_replace | 主なパラメータpath、old_str、new_str | 役割old_str を new_str に置換する |
create | 主なパラメータpath、file_text | 役割新しいファイルを作る |
insert | 主なパラメータpath、insert_line、insert_text | 役割指定行の後ろにテキストを挿入する |
細部の仕様は4点です。
view_rangeは2つの整数の配列で、行番号は1始まりです。終了行に-1を指定すると末尾までを意味しますview_rangeはファイルにだけ効き、ディレクトリには効きませんstr_replaceのold_strは空白とインデントまで完全一致が必要ですinsert_lineは「その行の後ろに挿入する」行番号で、0ならファイルの先頭に入ります
Pythonでハンドラを書く
次のコードは、上の仕様を満たす最小のハンドラの一例です。match で振り分ける骨組みと、バックアップ・一意マッチの考え方は公式の実装例にあります。以下はそれを組み合わせた実装例です。作業ディレクトリの外に出るパスを拒否する処理と、view の行番号付き出力は、骨組みにない追加の設計です。
import os
import shutil
ROOT = os.path.realpath("workspace")
MAX_CHARS = 10000
def resolve(path):
full = os.path.realpath(os.path.join(ROOT, path))
if os.path.commonpath([ROOT, full]) != ROOT:
raise PermissionError("Permission denied: outside workspace")
return full
def backup(full):
if os.path.exists(full):
shutil.copyfile(full, full + ".backup")
def view(full, view_range):
if os.path.isdir(full):
return "\n".join(sorted(os.listdir(full)))
with open(full, encoding="utf-8") as f:
lines = f.read().splitlines()
start, end = (view_range or [1, -1])
end = len(lines) if end == -1 else end
body = "\n".join(
f"{n}: {line}"
for n, line in enumerate(lines, 1)
if start <= n <= end
)
return body[:MAX_CHARS]続きは書き込み系の3コマンドです。
def str_replace(full, old, new):
with open(full, encoding="utf-8") as f:
text = f.read()
count = text.count(old)
if count == 0:
raise ValueError("No match found for replacement.")
if count > 1:
raise ValueError(f"Found {count} matches. Add more context.")
backup(full)
with open(full, "w", encoding="utf-8") as f:
f.write(text.replace(old, new))
return "Successfully replaced text"
def create(full, file_text):
backup(full)
os.makedirs(os.path.dirname(full), exist_ok=True)
with open(full, "w", encoding="utf-8") as f:
f.write(file_text)
return "File created"
def insert(full, line_no, text):
with open(full, encoding="utf-8") as f:
lines = f.read().splitlines(keepends=True)
backup(full)
lines.insert(line_no, text if text.endswith("\n") else text + "\n")
with open(full, "w", encoding="utf-8") as f:
f.writelines(lines)
return "Text inserted"
def handle_editor_tool(params):
command = params.get("command", "")
full = resolve(params.get("path", ""))
match command:
case "view":
return view(full, params.get("view_range"))
case "str_replace":
return str_replace(full, params["old_str"], params["new_str"])
case "create":
return create(full, params["file_text"])
case "insert":
return insert(full, params["insert_line"], params["insert_text"])
raise ValueError(f"Unknown command: {command}")str_replace が置換の前に出現回数を数えているのは、公式が注意点に挙げる「置換は必ず1箇所に一致させる」を満たすためです。複数一致を返したときの挙動と対処は、text_editorのstr_replaceで複数マッチエラーを防ぐ実装で個別に掘り下げています。
会話ループとエラーの返し方
ハンドラを呼ぶ側は、stop_reason が tool_use の間ループを回します。tool_use ブロックごとにハンドラを実行し、結果を tool_result として次の user メッセージに入れて返します。
import anthropic
client = anthropic.Anthropic()
tools = [{
"type": "text_editor_20250728",
"name": "str_replace_based_edit_tool",
"max_characters": MAX_CHARS,
}]
messages = [{"role": "user",
"content": "primes.py の構文エラーを直してください"}]
while True:
resp = client.messages.create(
model="claude-opus-5-5", max_tokens=1024,
tools=tools, messages=messages,
)
messages.append({"role": "assistant", "content": resp.content})
if resp.stop_reason != "tool_use":
break
results = []
for block in resp.content:
if block.type != "tool_use":
continue
try:
out, is_error = handle_editor_tool(block.input), False
except Exception as e:
out, is_error = f"Error: {e}", True
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": out,
"is_error": is_error,
})
messages.append({"role": "user", "content": results})tool_result には tool_use_id と content を入れ、失敗したときは任意項目の is_error を true にします。想定されるエラーは4種類です。
| 状況 | 返すメッセージの例 |
|---|---|
| ファイルが存在しない | 返すメッセージの例Error: File not found |
| 置換対象が複数に一致 | 返すメッセージの例Error: Found 3 matches for replacement text. Please provide more context to make a unique match. |
| 置換対象に一致なし | 返すメッセージの例Error: No match found for replacement. Please check your text and try again. |
| 権限エラー | 返すメッセージの例Error: Permission denied. Cannot write to file. |
いずれも is_error: true を付けて返します。文言そのものは例示で、Claudeが読んで次の手を決められる内容であれば構いません。ループでは、例外を一括で is_error に変換しています。ハンドラ側は失敗したら例外を投げるだけで済みます。
1往復分の入出力を見る
壊れた primes.py を直す場面で、ハンドラが受け取る入力と返す中身を1往復分並べます。最初にClaudeが返すのは、ファイルを開く要求です。stop_reason は tool_use になります。
{
"type": "tool_use",
"id": "toolu_01AbCdEfGhIjKlMnOpQrStU",
"name": "str_replace_based_edit_tool",
"input": {"command": "view", "path": "primes.py"}
}ハンドラは行番号付きの本文を content に入れて返します。先頭の数行はこうなります。
1: def is_prime(n):
2: "Check if a number is prime."
3: if n <= 1:行番号は必須ではありませんが、あると次の str_replace や insert で位置を指定しやすくなります。本文を読んだClaudeは、for 行のコロン抜けに気づいて置換を要求します。
{
"type": "tool_use",
"id": "toolu_01PqRsTuVwXyZAbCdEfGh",
"name": "str_replace_based_edit_tool",
"input": {
"command": "str_replace",
"path": "primes.py",
"old_str": " for num in range(2, limit + 1)",
"new_str": " for num in range(2, limit + 1):"
}
}old_str は行頭の空白まで含めて、ファイルの該当部分と一致させています。ハンドラが Successfully replaced text を返せば、Claudeは結果を踏まえて次の応答を返します。tool_use_id は要求ごとに変わるので、返す tool_result には必ず対応するIDを入れます。
undo_editはなぜ消えたのか
undo_edit は、直前の編集を取り消すコマンドでした。現行の text_editor_20250728 には存在しません。バージョンごとの履歴は次のとおりです。
text editor toolのバージョン履歴
- 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_20250429 から消え、text_editor_20250728 も同じ」という事実までです。
実装上の要点は2つあります。
text_editor_20250728を使うなら、undo_editのハンドラは不要です。Claudeはこのコマンドを呼びません- 取り消しが必要なのはアプリケーションの都合なので、自前の仕組みで持ちます
後者の手がかりが、注意点にも挙がるバックアップです。編集の前にファイルのコピー(上のコードでは .backup)を作っておけば、利用者向けの「元に戻す」操作をアプリケーションから提供できます。上の例は直前の1世代しか残さないので、複数回さかのぼりたい場合は世代管理やGitのコミットに置き換えます。
バージョンの選び方
ツール一覧では、text_editor_20250728 がClaude 4以降のモデル向け、text_editor_20250124 が以前のモデル向けとされています。つまり、使うバージョンは対象モデルで決まります。text_editor_20250124 は undo_edit を持ち、max_characters は使えません。切り替えるのはタイプだけではありません。ツール名も変わり、text_editor_20250124 では str_replace_editor、text_editor_20250728 では str_replace_based_edit_tool です。宣言の名前と食い違えば、tool_use の name も食い違います。複数世代のモデルをまたいで同じ基盤を動かす場合は、モデルごとにタイプと名前を切り替え、undo_edit と max_characters の有無をハンドラ側で吸収する必要があります。text_editor_20241022 の名前は、参照した資料に載っていません。
実装前に確認したい点
実装時の注意は次の4点です。
- セキュリティ。ツールはローカルのファイルシステムに触れるので、適切な対策を入れる
- バックアップ。重要なファイルは編集の前にコピーを取る
- 入力の検証。意図しない変更を防ぐため、受け取った入力を検証する
- 一意なマッチ。置換が必ず1箇所に一致するようにする
このうち1と3を実装に落とすと、前掲の resolve のようなパスの検証になります。Claudeが返す path は、モデルの出力であって信頼できる入力ではありません。../ を含むパスや、シンボリックリンク経由で作業ディレクトリの外を指すパスは、realpath で正規化してから範囲を確かめます。
プロンプトの側にも工夫の余地があります。ベストプラクティスでは、「コードを直して」ではなく「primes.pyに構文エラーがあって動きません。直してください」のように、対象のファイルと症状を具体的に書くよう勧めています。ファイルパスを明示すれば、Claudeが無駄な view を重ねずに済みます。
試すときの流れ
動作確認は、壊れたファイルを1つ用意するのが手早い方法です。
動作確認の進め方
- 1
作業ディレクトリを用意する
workspace/primes.pyに、for行の末尾のコロンを抜いたコードを置きます。 - 2
ループを実行する
上のスクリプトを実行し、Claudeが
viewで中身を確認してからstr_replaceを呼ぶことを確かめます。 - 3
ログを見る
各
tool_useのinputと、返したtool_resultのcontentを出力して、max_charactersでの切り詰めやis_errorの動きを追います。
自前実装のまとめ
text_editor_20250728 の自前実装は、ツール宣言1つとコマンド4つのハンドラに分解できます。input_schema は書かず、max_characters の切り詰めは実装側の仕事で、取り消し機能は仕様の外にあります。実装の分かれ目は、パスの検証、一意マッチの確認、失敗時の is_error の3つです。複数世代のモデルに対応するときだけ、undo_edit と max_characters の差を吸収する層が増えます。