はじめに / 対象と前提
Claude に「前回の会話で決めたこと」を覚えさせたい。会話履歴を全部積み直すのは重いし、コンテキストが溢れる。そこで Anthropic が用意している Memory tool(memory_20250818)を Python から動かしてみた。
この記事は、Claude API のツール呼び出し(tool use)を一度は実装したことがある人向け。「Memory tool を宣言したのに何も保存されない」「/memories/../ で変なパスが飛んできた」あたりで詰まった人に刺さるはず。
環境は以下。
- Python 3.13
-
anthropicPython SDK 1.x - モデル:
claude-opus-5
TL;DR
- Memory tool は クライアント側ツール。API は 1 バイトも保存しない。ファイルの読み書きは自分で実装する
- ツールは
{"type": "memory_20250818", "name": "memory"}と宣言するだけ。input_schemaは書かない - Claude から
view/create/str_replace/insert/delete/renameの 6 コマンドが飛んでくるので、/memories配下のファイル操作に変換してtool_resultで返す - ハマりどころは「保存されない(自分で実装する必要がある)」「パストラバーサル」「system prompt で指示しないと読みに来ない」の 3 つ
手順 / 動かし方
1. まずは宣言だけして何が飛んでくるか見る
import anthropic
client = anthropic.Anthropic()
res = client.messages.create(
model="claude-opus-5",
max_tokens=4096,
tools=[{"type": "memory_20250818", "name": "memory"}],
messages=[{"role": "user", "content": "自分の好きな言語は Python。覚えておいて。"}],
)
for block in res.content:
print(block.type, getattr(block, "input", None))
実行結果(抜粋):
tool_use {'command': 'view', 'path': '/memories'}
いきなり create ではなく、まず /memories ディレクトリの view が来る。Claude は「今何が保存されているか」を確認してから書き込む動きをする。この時点で stop_reason は tool_use なので、こちらが結果を返さないと会話は進まない。
2. 6 コマンドをローカルファイルシステムに繋ぐ
最小実装はこれ。/memories を実際のディレクトリ(ここでは ./memory_store)にマッピングする。
from pathlib import Path
ROOT = Path("./memory_store").resolve()
ROOT.mkdir(exist_ok=True)
def resolve(virtual: str) -> Path:
if not virtual.startswith("/memories"):
raise ValueError(f"path must start with /memories: {virtual}")
real = (ROOT / virtual[len("/memories"):].lstrip("/")).resolve()
if ROOT != real and ROOT not in real.parents:
raise ValueError(f"path escapes memory root: {virtual}")
return real
def handle_memory(inp: dict) -> str:
cmd = inp["command"]
if cmd == "view":
p = resolve(inp["path"])
if p.is_dir():
files = sorted(str(f.relative_to(ROOT)) for f in p.rglob("*") if f.is_file())
return "Directory: /memories\n" + "\n".join(f"- /memories/{f}" for f in files)
lines = p.read_text().splitlines()
start, end = inp.get("view_range", [1, len(lines)])
return "\n".join(f"{i}: {l}" for i, l in enumerate(lines[start-1:end], start))
if cmd == "create":
p = resolve(inp["path"]); p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(inp["file_text"]); return f"File created: {inp['path']}"
if cmd == "str_replace":
p = resolve(inp["path"]); s = p.read_text()
if s.count(inp["old_str"]) != 1:
raise ValueError("old_str must appear exactly once")
p.write_text(s.replace(inp["old_str"], inp["new_str"])); return "Edited"
if cmd == "insert":
p = resolve(inp["path"]); lines = p.read_text().splitlines()
lines[inp["insert_line"]:inp["insert_line"]] = inp["insert_text"].splitlines()
p.write_text("\n".join(lines) + "\n"); return "Inserted"
if cmd == "delete":
p = resolve(inp["path"]); p.unlink() if p.is_file() else __import__("shutil").rmtree(p)
return f"Deleted: {inp['path']}"
if cmd == "rename":
resolve(inp["old_path"]).rename(resolve(inp["new_path"])); return "Renamed"
raise ValueError(f"unknown command: {cmd}")
3. ループを回して動作確認
SYSTEM = (
"あなたには /memories ディレクトリの memory ツールがある。"
"会話の最初に必ず /memories を view して過去の記録を確認し、"
"ユーザーの好みや決定事項は /memories/notes.md に記録すること。"
)
def chat(user_text: str) -> str:
messages = [{"role": "user", "content": user_text}]
while True:
res = client.messages.create(
model="claude-opus-5", max_tokens=4096, system=SYSTEM,
tools=[{"type": "memory_20250818", "name": "memory"}],
messages=messages,
)
messages.append({"role": "assistant", "content": res.content})
if res.stop_reason != "tool_use":
return "".join(b.text for b in res.content if b.type == "text")
results = []
for b in res.content:
if b.type != "tool_use":
continue
try:
out, err = handle_memory(b.input), False
except Exception as e:
out, err = str(e), True
results.append({"type": "tool_result", "tool_use_id": b.id,
"content": out, "is_error": err})
messages.append({"role": "user", "content": results})
print(chat("自分の好きな言語は Python。覚えておいて。"))
# --- プロセスを再起動してから ---
print(chat("自分の好きな言語って何だっけ?"))
2 回目の呼び出しで、Claude は view /memories → view /memories/notes.md を経て「Python です」と答えた。会話履歴を渡していないのに答えられている、つまりファイル経由で記憶が引き継がれたことが確認できた。
memory_store/notes.md の中身:
# ユーザーの好み
- 好きな言語: Python
ハマりどころ
1. 宣言しただけでは何も保存されない
自分が最初に勘違いした点。「Anthropic 側にストレージがあって、memory_20250818 を付けたら勝手に永続化される」と思っていた。
実際は web_search のようなサーバー側ツールではなく、bash や text_editor と同じクライアント側ツール。tool_use が返ってきたら自分でファイルを触り、tool_result を返さない限り何も起きない。ステップ 1 の結果で stop_reason が tool_use のまま止まるのがその証拠。
ちなみに、以前は context-management-2025-06-27 のベータヘッダーが必須だったが、今の SDK では通常の client.messages.create で動く。古い記事のまま betas=[...] を付けても害はないが、不要。
2. パストラバーサルは自分で防ぐ
Claude が /memories/../../.env のようなパスを返してくる可能性はゼロではない(プロンプトインジェクションで誘導されるケースを想定する)。API 側では検証されないので、resolve() で 必ず /memories プレフィックスと、実パスがルート配下に収まっているかの 2 段チェック を入れる。
上のコードで ROOT not in real.parents を見ているのがそれ。Path.resolve() を通してから比較しないと、シンボリックリンクや .. を素通しする。
あとメモリファイルに API キーやパスワードを書かせない。system prompt で「秘密情報は記録しない」と明示しておくと安全側に倒れる。
3. system prompt で指示しないと読みに来ない
ツールを宣言しただけだと、Claude は「覚えて」と言われたときは create するが、次のセッションで 自発的に view しに来ないことがある。特に質問が短いと、メモリを見ずに「分かりません」と返す。
対策は上の SYSTEM のように「会話の最初に /memories を view しろ」「何をどのファイルに書け」を明示すること。書き込み先のファイル名を固定しておくと、セッションごとに notes.md と user_preferences.md が乱立するのも防げる。
補足:SDK のヘルパーを使う
自前ループが面倒なら、SDK に BetaAbstractMemoryTool という抽象クラスがある。6 コマンド分のメソッドを実装して tool_runner に渡せば、ループもディスパッチも SDK がやってくれる。
from anthropic.lib.tools import BetaAbstractMemoryTool
class LocalMemory(BetaAbstractMemoryTool):
def view(self, command): ...
def create(self, command): ...
def str_replace(self, command): ...
def insert(self, command): ...
def delete(self, command): ...
def rename(self, command): ...
runner = client.beta.messages.tool_runner(
model="claude-opus-5", max_tokens=4096,
tools=[LocalMemory()],
messages=[{"role": "user", "content": "自分の好きな言語って何だっけ?"}],
)
for message in runner:
print(message)
ただしパスの検証は結局自分で書くことになる。SDK が肩代わりしてくれるのはループ部分だけ、と理解しておくとよい。
まとめ
- Memory tool は クライアント側ツール。API は何も保存しない、ファイル操作は自分で書く
- 6 コマンド(
view/create/str_replace/insert/delete/rename)を/memories配下の実ファイルに変換してtool_resultで返す -
パストラバーサル対策は必須。
resolve()後にルート配下かを検証する - system prompt で「最初に view しろ」「どのファイルに書け」を明示しないと、次のセッションで読みに来ない
- ループが面倒なら
BetaAbstractMemoryTool+tool_runner