Claude Code の auto memory は会話をまたいで知識を永続化する仕組みですが、一度書いたエントリは自動でリフレッシュされません。削除済みのファイルへのリンク、半年前の構成メモ、役割が変わった前提知識——それらが今もセッション開始時に読み込まれ、Claude の判断を静かに歪め続けていませんか。
本稿では 2026-08-01 時点の Claude Code v2.1.214+ を前提に、3 種類の劣化を機械的に検査する Python スクリプトを完全実装します。
MEMORY.md のロード仕様
"MEMORY.md acts as an index of the memory directory. Claude reads and writes files in this directory throughout your session, using MEMORY.md to keep track of what's stored where."
Memory — Claude Code
MEMORY.md はセッション開始時に自動ロードされるインデックスファイルです。ただしロードされるのは先頭 200 行または 25KB に限られます。
"The first 200 lines of MEMORY.md , or the first 25KB, whichever comes first, are loaded at the start of every conversation. Content beyond that threshold is not loaded at session start."
Memory — Claude Code
この計測はファイルの生データに対して行われるのではなく、YAML フロントマターとブロックレベル HTML コメントを除去した後のコンテンツに対して行われます。
"YAML frontmatter and block-level HTML comments are stripped before the index is loaded, so they don't count toward the limits."
Memory — Claude Code
v2.1.211 より前はファイルの生データを計測していたため、コメントがあるだけで誤エラーが発生していました。
"Before v2.1.211, Claude Code measured the raw file, and frontmatter or comments could trigger the error even when the loaded content fit."
Memory — Claude Code
バリデーターが正確な使用率を報告するには、Claude Code と同じ前処理を再現する必要があります。
modified フィールドと鮮度管理
v2.1.214 から、YAML フロントマターを持つ topic ファイルに Claude が書き込むと、modified フィールドに ISO 8601 タイムスタンプが記録されます。
"When Claude writes a memory file that begins with YAML frontmatter, Claude Code records the write time in a modified frontmatter field as an ISO 8601 timestamp. The timestamp shows how current the fact is, both to you and to Claude when it reads the memory back."
Memory — Claude Code
"The modified field requires Claude Code v2.1.214 or later."
Memory — Claude Code
modified フィールドが存在しないファイルは、v2.1.214 より前に作成されたか、フロントマターを持たないファイルです。「タイムスタンプなし」と「陳腐化」は区別すべき別の状態です。
topic ファイルの特性
MEMORY.md から参照される topic ファイルはセッション開始時には読み込まれません。
"Topic files like debugging.md or patterns.md are not loaded at startup. Claude reads them on demand using its standard file tools when it needs the information."
Memory — Claude Code
これはリンク切れが起きても、Claude が実際にそのファイルを参照しようとするまで発覚しないことを意味します。また auto memory はマシンローカルです。
"Auto memory is machine-local. All worktrees and subdirectories within the same git repository share one auto memory directory. Files are not shared across machines or cloud environments."
Memory — Claude Code
validate_memory.py 完全実装
Python 3.10 以上、Claude Code v2.1.214 以上で動作確認済み(2026-08-01 時点)。
#!/usr/bin/env python3
"""
validate_memory.py — Claude Code auto memory health checker
Usage: python3 validate_memory.py [memory_dir]
Checks:
1. MEMORY.md line / byte limit usage (after stripping frontmatter+comments)
2. Broken links (referenced .md files that no longer exist)
3. Stale entries (modified timestamp older than STALE_DAYS)
Requires: Python 3.10+, Claude Code v2.1.214+ (for modified field support)
"""
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
LINE_LIMIT = 200
BYTE_LIMIT = 25 * 1024 # 25 KB
WARN_RATIO = 0.85 # flag at 85% usage
STALE_DAYS = 30 # flag entries with modified older than N days
def default_memory_dir() -> Path:
"""Derive the memory directory from the git root of the current directory."""
try:
import subprocess
r = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
capture_output=True, text=True,
)
root = r.stdout.strip() if r.returncode == 0 else str(Path.home())
except FileNotFoundError:
root = str(Path.home())
project_key = root.replace("/", "-") # e.g. /home/alice -> -home-alice
return Path.home() / ".claude" / "projects" / project_key / "memory"
def strip_for_load(text: str) -> str:
"""Remove YAML frontmatter and block-level HTML comments, as Claude Code does."""
if text.startswith("---"):
end = text.find("\n---", 3)
if end != -1:
text = text[end + 4:]
return re.sub(r"<!--.*?-->", "", text, flags=re.DOTALL)
def check_limits(index: Path) -> list[str]:
"""Return [WARN]/[OVER] messages for line and byte usage of the loaded content."""
issues: list[str] = []
loaded = strip_for_load(index.read_text(encoding="utf-8"))
lines = loaded.splitlines()
nb = len(loaded.encode("utf-8"))
lr, br = len(lines) / LINE_LIMIT, nb / BYTE_LIMIT
if lr >= 1.0:
issues.append(
f"[OVER] line limit: {len(lines)}/{LINE_LIMIT} lines "
f"— entries past line {LINE_LIMIT} are dropped on load"
)
elif lr >= WARN_RATIO:
issues.append(f"[WARN] line usage: {len(lines)}/{LINE_LIMIT} ({lr * 100:.0f}%)")
if br >= 1.0:
issues.append(f"[OVER] byte limit: {nb:,}/{BYTE_LIMIT:,} bytes")
elif br >= WARN_RATIO:
issues.append(f"[WARN] byte usage: {nb:,}/{BYTE_LIMIT:,} B ({br * 100:.0f}%)")
return issues
def extract_refs(content: str) -> list[str]:
"""Return all (*.md) targets from Markdown links in the index."""
return re.findall(r"\[.*?\]\(([^)]+\.md)\)", content)
def read_modified(path: Path) -> datetime | None:
"""Return the modified timestamp from YAML frontmatter, or None."""
try:
text = path.read_text(encoding="utf-8")
if not text.startswith("---"):
return None
end = text.find("\n---", 3)
if end == -1:
return None
m = re.search(r"^modified:\s*(.+)$", text[3:end], re.MULTILINE)
if not m:
return None
ts = datetime.fromisoformat(m.group(1).strip())
return ts if ts.tzinfo else ts.replace(tzinfo=timezone.utc)
except Exception:
return None
def main() -> int:
memory_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else default_memory_dir()
index = memory_dir / "MEMORY.md"
if not index.exists():
print(f"[ERROR] not found: {index}")
return 1
now = datetime.now(timezone.utc)
issues: list[str] = []
stat = dict(ok=0, stale=0, missing=0, no_ts=0)
# 1. index size limits
issues.extend(check_limits(index))
# 2. file references
loaded_content = strip_for_load(index.read_text(encoding="utf-8"))
refs = extract_refs(loaded_content)
for ref in refs:
target = memory_dir / ref
if not target.exists():
issues.append(f"[MISSING] {ref}")
stat["missing"] += 1
continue
ts = read_modified(target)
if ts is None:
issues.append(f"[NO_TS] {ref} (no modified field — needs v2.1.214+)")
stat["no_ts"] += 1
elif (now - ts).days > STALE_DAYS:
issues.append(
f"[STALE] {ref} — {(now - ts).days}d old (modified: {ts.date()})"
)
stat["stale"] += 1
else:
stat["ok"] += 1
# 3. report
print(
f"validate_memory {now.strftime('%Y-%m-%dT%H:%M:%SZ')}\n"
f"index {index}\n"
f"refs {len(refs)} entries "
f"ok={stat['ok']} stale={stat['stale']} "
f"missing={stat['missing']} no_ts={stat['no_ts']}\n"
)
if not issues:
print(" OK — no issues found")
return 0
for issue in issues:
print(f" {issue}")
return 1
if __name__ == "__main__":
sys.exit(main())
動作確認
正常な状態の出力例:
$ python3 validate_memory.py
validate_memory 2026-08-01T09:12:03Z
index /home/alice/.claude/projects/-home-alice/memory/MEMORY.md
refs 18 entries ok=18 stale=0 missing=0 no_ts=0
OK — no issues found
$ echo $?
0
問題が検出された場合の出力例:
$ python3 validate_memory.py
validate_memory 2026-08-01T09:14:22Z
index /home/alice/.claude/projects/-home-alice/memory/MEMORY.md
refs 34 entries ok=28 stale=3 missing=1 no_ts=2
[WARN] line usage: 178/200 (89%)
[MISSING] old-setup-notes.md
[STALE] initial-env-config.md — 47d old (modified: 2026-06-15)
[STALE] api-key-rotation.md — 38d old (modified: 2026-06-24)
[STALE] project-context-v1.md — 35d old (modified: 2026-06-27)
[NO_TS] debugging-patterns.md (no modified field — needs v2.1.214+)
[NO_TS] old-workflow-notes.md (no modified field — needs v2.1.214+)
$ echo $?
1
問題が1件でも検出されると終了コード 1 が返るため、cron や CI での異常検知に使えます。
cron による週次自動チェック
# crontab -e に追加
# 毎週月曜 9:00 JST (= UTC 0:00)
0 0 * * 1 python3 ~/scripts/validate_memory.py >> ~/logs/memory-validate.log 2>&1
ログファイルは日付ヘッダーが入るため、後から検索可能です。
設計上の判断: 公式ドキュメントに書かれていない3つの落とし穴
1. 生ファイルを計測してはいけない
スクリプトの核心は strip_for_load() 関数です。Claude Code は v2.1.211 以降、YAML フロントマターとブロックレベル HTML コメントを除去した後のコンテンツに対して 200行/25KB を計測します。生ファイルのバイト数を計測すると、実際には制限内でも「超過」と誤判定します。これは v2.1.211 以前のバグがまさにこの原因で引き起こされていたためです。バリデーターが本番の計測ロジックを再現しなければ、警告の意味が失われます。
2. NO_TS と STALE は別の問題
modified フィールドを持たないファイル(NO_TS)は古いとは限りません。v2.1.214 より前に作成されたか、フロントマターを持たない形式で書かれているだけかもしれません。自動的に削除候補として扱うと、有効なメモリが失われます。NO_TS は「タイムスタンプを追加して欲しい」という通知であり、STALE は「内容が古い可能性がある」という通知です。この区別をレポートに明示することで、誤削除を防げます。
3. マシンローカルの意味: 環境ごとに検査が要る
auto memory はマシンローカルです。WSL と AWS の開発環境を両方使っている場合、一方で最新化したメモリが他方には存在しません。「WSL では問題なかった」は AWS での健全性を保証しません。validate_memory.py を両環境の cron に設置し、それぞれ独立して実行することが重要です。単一の検査結果を「全環境のメモリが健全」と読み替えないようにしてください。
既知の限界
-
extract_refs()は[text](file.md)形式のリンクのみを抽出します。ファイル名がプレーンテキストで記載されている場合は検出されません。 -
modifiedフィールドがあっても、Claude が内容を更新せずに別の理由でファイルに触れた場合はタイムスタンプが更新されます(偽陰性の可能性)。 - STALE_DAYS = 30 は任意の閾値です。メモリの内容変化速度に合わせて調整してください。