Claude Code 2.1.312 の changelog を読んでいて、ある一行で手が止まりました。「とても大きな CLAUDE.md や rules ファイルを、先頭しか見ないまま Claude が上書きしていた」という修正です。自分の CLAUDE.md が何文字あるのかを、その場で答えられませんでした。
個人開発のアプリの運用ルールや、サイトの更新手順を書き足し続けた私自身の CLAUDE.md も、気づくと全体が見渡せなくなっていました。そこへ「整理しておいて」と頼む。便利な頼み方ですが、相手が前半しか読んでいなければ、後半は存在しないものとして扱われかねません。
最初にお伝えしたいのは、2.1.312 以降は Write が「全部読むか、その場で編集するか」を Claude に求めるようになったということです。ただ、版を上げていない環境や、チームの誰かの手元では、まだ起きうる話です。そこで、起きたことに気づく方法、戻す方法、次から止める方法を、順に書き残します。
なお、Claude Code 自体の挙動は changelog の記述に沿ったもので、不具合の再現は試しておりません。手元で確かめたのは、受け止める側の道具です。12 章・8,552 文字の検証用 CLAUDE.md を作り、後半を落として書き戻した状態を作って、下のスクリプトを通しました。
なぜ「後半だけ消える」ことが起きるのか
仕組みは単純です。大きな指示ファイルは、Claude に先頭の一部だけが渡されることがあります。サブフォルダの CLAUDE.md に文字数の上限がかかる話は、サブフォルダの CLAUDE.md の後半だけが効かないとき、数えるのは容量ではなく文字数でしたに書きました。
見えているのが先頭だけの状態で、Claude が「このファイルを整理して書き直そう」と考え、Write で全文を置き換えたとします。すると、見えていなかった後半は、書き込む内容の中にそもそも含まれないのです。編集ではなく置換になるので、消えたことを知らせる警告も出ません。
changelog が対象に挙げているのは、CLAUDE.md、rules ファイル、そして @ で取り込まれたファイルのうちサブフォルダにあるものです。同じ症状は、この範囲の外でも、別の理由で起きうると考えておくほうが安全です。
手順1:版と、指示ファイルの大きさを確かめます
まず版です。2.1.312 より前なら、次の手順で網を張っておく価値があります。
claude --version次に、プロジェクトの指示ファイルを大きい順に並べます。日本語のファイルは wc -c(バイト数)だと実際の文字数の 2〜3 倍に見えるので、wc -m をロケール固定で使います。
# 何を解決するか: どの指示ファイルが「先頭しか読まれない」大きさか、先に把握する
find . -type f \( -name CLAUDE.md -o -path '*/.claude/rules/*.md' \) \
-not -path '*/node_modules/*' | while read -r f; do
printf '%7s 文字 %s\n' "$(LC_ALL=C.UTF-8 wc -m < "$f")" "$f"
done | sort -rn | head検証用のファイルでは、次のように出ました。同じファイルは、wc -c だと 22,552 でした。
8552 文字 ./sub/CLAUDE.md
8552 文字 ./sub/.claude/rules/a.md
8552 文字 ./CLAUDE.mdLC_ALL=C.UTF-8 を付けているのは、ロケールが C のままだと wc -m もバイトを数えてしまうためです。この罠は、先ほどの記事でも手を取られたところでした。
手順2:消えたかどうかを、見出しで確かめます
「何かおかしい」と感じたとき、最初に見るのは git の差分の形です。置換で後半が落ちたファイルは、追加がごくわずかで、削除だけが大きく並びます。
git diff --numstat
# 1 138 CLAUDE.md ← 追加 1 行、削除 138 行追加が 1 行で削除が 138 行。この形を見たら、整理ではなく切り落としを疑います。内容を目で追う前に、消えた見出しを機械的に出してしまうのが早道です。
#!/usr/bin/env bash
# lost_headings.sh — 基準の版にあって、いまのファイルから消えた見出しを出す
# 使い方: lost_headings.sh CLAUDE.md [基準のリビジョン(既定 HEAD)]
set -euo pipefail
f="${1:?file}"; rev="${2:-HEAD}"
old="$(mktemp)"; trap 'rm -f "$old"' EXIT
git show "${rev}:${f}" > "$old"
echo "行数: $(wc -l < "$old") -> $(wc -l < "$f")"
echo "文字数: $(LC_ALL=C.UTF-8 wc -m < "$old") -> $(LC_ALL=C.UTF-8 wc -m < "$f")"
echo "--- 消えた見出し ---"
diff <(grep -E '^#{1,4} ' "$old") <(grep -E '^#{1,4} ' "$f") | grep '^<' | sed 's/^< //' || true検証用の切り落とし状態に当てた結果です。
行数: 206 -> 68
文字数: 8552 -> 2850
--- 消えた見出し ---
## 第5章 ルール5
## 第6章 ルール6
(中略)
## 第12章 ルール12見出しで比べる理由は、本文の言い換えと区別がつくからです。Claude が文言を整えただけなら見出しは残ります。見出しが丸ごと消えているなら、それは整えたのではなく、落ちたのです。
手順3:戻します
差分が切り落としだけなら、コミット済みの版へ戻すのが最短です。
git restore CLAUDE.mdただし、コミットしていない自分の編集も同じファイルに入っていた場合は、git restore でそれも消えます。その場合は、元の版を別名で取り出し、消えた章だけを手で戻します。
git show HEAD:CLAUDE.md > /tmp/CLAUDE.old.md
# 消えた章だけを /tmp/CLAUDE.old.md から開いて、いまのファイルに貼り直すgit で管理していないフォルダなら、Dropbox や iCloud などの同期サービスの版履歴が最後の砦になります。ただ、版履歴は探し当てるまでに時間がかかります。指示ファイルはかならず git に入れる——これだけは先に決めておくのが、結局いちばん楽です。
手順4:次から止めます
戻せたあとは、同じことが起きても被害が出ない形にしておきます。効果の大きい順に 3 つです。
1つ目は、頼む前にコミットすることです。差分が出るのは、基準があるからです。「整理してもらう」前の 1 コミットが、いちばん安い保険になります。
2つ目は、頼み方です。「この節だけ Edit で直して。全文の書き直しはしないで」と一言添えます。Edit はその場の置き換えなので、読んでいない部分には触れません。
3つ目は、Write を止めるフックです。指示ファイルを全文置換しようとして、見出しが消える、または大きく縮むときだけ、Claude に差し戻します。
#!/usr/bin/env python3
# .claude/hooks/guard_instruction_rewrite.py
# 何を解決するか: 指示ファイルが Write で全文置換され、後半が消えるのを書き込み前に止める
import json, os, re, sys
WATCHED = re.compile(r"(^|/)(CLAUDE(\.local)?\.md|\.claude/rules/.+\.md)$")
MIN_CHARS = 6000 # これより小さいファイルは素通し
SHRINK_LIMIT = 0.7 # 新しい内容が既存の 70% 未満なら止める
def headings(text):
return [l.strip() for l in text.splitlines() if re.match(r"#{1,4} ", l)]
def main():
# stdin はバイトで受けて UTF-8 に固定する(Windows の cp932 対策)
data = json.loads(sys.stdin.buffer.read().decode("utf-8"))
if data.get("tool_name") != "Write":
return 0
inp = data.get("tool_input", {})
path = inp.get("file_path", "")
if not WATCHED.search(path.replace("\\", "/")) or not os.path.isfile(path):
return 0
old = open(path, encoding="utf-8", errors="replace").read()
new = inp.get("content", "")
if len(old) < MIN_CHARS:
return 0
lost = [h for h in headings(old) if h not in set(headings(new))]
ratio = len(new) / len(old)
if lost or ratio < SHRINK_LIMIT:
print(f"[guard] {path}: 既存 {len(old)} 文字 → 新規 {len(new)} 文字"
f"({ratio:.0%})、消える見出し {len(lost)} 件。"
"全文を Read で読み直すか、Edit で該当箇所だけ直してください。",
file=sys.stderr)
return 2 # 終了コード 2 で書き込みを止め、stderr を Claude に返す
return 0
if __name__ == "__main__":
sys.exit(main()).claude/settings.json には、Write だけを対象に登録します。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard_instruction_rewrite.py\""
}
]
}
]
}
}標準入力に模擬のリクエストを流して確かめた結果は、次のとおりです。
| 書き込もうとした内容 | 終了コード | 理由 |
|---|---|---|
| 先頭の 3 分の 1 だけ | 2(止まる) | 33% に縮み、見出し 8 件が消える |
| 同じ内容のまま | 0(通る) | 変化なし |
| 末尾に 1 節を追記 | 0(通る) | 見出しは減らず、縮まない |
| 第 5 章の見出し 1 行だけ削除 | 2(止まる) | 100% でも見出しが消える |
| CLAUDE.md 以外のファイル | 0(通る) | 対象外 |
見出しを 1 行消しただけでも止まる点は、意図したものです。意図して章を削るときは、フックを一時的に外すか、Edit で消す運用にしています。
落とし穴
フックが守れるのは Write だけです。Bash の cat > CLAUDE.md のようなリダイレクトは、matcher が違うので素通りします。そこまで締めたい場合は、拒否ルールとの組み合わせを検討することになります。
もうひとつは、フックそのものが静かに効いていない可能性です。日本語のパスを扱うフックが一度も止めてくれなかった経緯は、日本語フォルダ名を守るはずのフックが、一度も止めてくれませんでしたに書きました。設定を足したあとは、かならず模擬の入力で一度止めてみてください。設定の JSON を壊したときの復旧は、壊れた settings.json で Claude Code が起動しなくなったときにまとめています。
しきい値の MIN_CHARS と SHRINK_LIMIT は、私の検証用ファイルに合わせた値です。ご自身のファイルの大きさを手順1で見たうえで、調整していただければ幸いです。
次に、1つだけ
全部読んでいない文書は、部分でしか直さないことにします。 この線引きを、頼み方の側にも、道具の側にも置いておきたいと考えています。
まずは手順1の find を 1 回だけ流して、いちばん大きい指示ファイルの文字数を確かめ、そのファイルをコミットしてください。フックを入れるのは、そのあとで十分です。検証用の環境でも、この順番で整えてまいりました。