~/posts / agent-docs-rot
CLAUDE.mdに書いた規約は、実装から静かに剥がれていく
●
Claude Code に CLAUDE.md を書いて運用している人向け。個人プロジェクト5つ分の設定を一斉に見直したら、コードではなく規約の側が壊れていた。しかも4種類とも、エラーが出ない壊れ方だった。
先に結論を書く。規約は実装から剥がれる。剥がれること自体は防げないので、剥がれたと気づける仕組みをテストとして足すしかない。
前提
- 2026年7月時点。Claude Code のデスクトップ版、Windows 11
- 対象は個人プロジェクト5リポジトリ。Next.js の静的サイト、Python のデスクトップアプリなど
- 想定読者は、
CLAUDE.mdをすでに書いていて、そこそこ育ってきた人
まだ CLAUDE.md が数百行の段階なら、この記事の話はまだ起きていない。半年運用したあたりから効いてくる。
規約が実装と食い違う
静的エクスポートしている Next.js のサイトで、規約ドキュメントを現行コードから書き直した。すると規約側の誤りが3件見つかった。
| 規約に書いてあったこと | 実態 |
|---|---|
全 page に export const dynamic = "force-static" を付ける |
src/app/**/page.tsx 46ファイルに1件も無い。output: "export" では全ルートが静的化されるので不要 |
page.tsx に metadata を書く |
page はほぼ "use client" なので export できない。実際は同階層の layout.tsx が持っている(40ファイル) |
pnpm test で全テストが走る |
vitest の projects は3つ。うち1つは Playwright + Chromium を起動する |
1つ目が一番危なかった。この規約に従って SEO 担当のサブエージェントに「指定漏れの監査」をさせていたら、46ページ全部に無意味な行が足されていた。テストは通る。ビルドも通る。差分レビューでも「規約どおりですね」で終わる。
人間が規約を読むときは、書いてあることと目の前のコードが食い違えば手が止まる。AI は止まらない。規約ドキュメントは、AI に渡した瞬間から読み物ではなく実行される仕様になる。にもかかわらず、実装と同期させる仕組みは誰も付けていない。
3つ目には副作用もあった。以前「storybook のテストは環境依存で落ちるから除外する」という回避策をメモに残していたのだが、その回避策が worker プロジェクトまで巻き込んで落としていた。回避策そのものが腐っていた形になる。
量が誰にも見えていない
別のプロジェクト(Python のデスクトップアプリ)では、中身ではなくサイズが問題になっていた。
2026-07-21 29KB
2026-07-27 55KB (+90%)
6日で倍近い。しかも増分の68%が「設計上の判断」という1つの節だった。実測値や失敗の記録を、その都度 CLAUDE.md に追記していた結果だ。
CLAUDE.md は全セッションの先頭に丸ごと載る。毎回のコンテキストを固定で食う。
問題は、これが膨らんでいることに誰も気づけない構造になっていることだ。+2KB のコミットは毎回まっとうに見えるし、実際1つ1つの追記は正しかった。差分レビューでは増分しか見えず、総量には誰も気づかない。正しい追記が積み上がって全体が壊れるのは、レビューが構造的に苦手なやつだと思う。
予算をテストで縛る
分割して26KBまで落としたあと、同じことがまた起きたら気づけるように、サイズの上限をテストにした。
# 分割直後 26.2KB に対する上限。伸びしろは残すが、倍になる前に止まる。
CLAUDE_MD_BUDGET = 34_000
def test_claude_md_stays_within_budget():
size = len(CLAUDE_MD.read_bytes())
assert size <= CLAUDE_MD_BUDGET, (
f"CLAUDE.md が {size} バイトで予算 {CLAUDE_MD_BUDGET} を超えた。"
"行を削るのではなく、実測値や失敗の記録を docs/design-rationale.md へ移すこと。"
)
工夫したのは assert メッセージだ。「超えた」とだけ言われると、人は行を削る。削られて困るのは、たいてい一番大事な「なぜそうしたか」の部分になる。だから逃がし先を先に書いておく。
分割そのものにも一手間かけた。CLAUDE.md の太字は「ここは地雷」という標識として使っていたので、太字179件を分割前後で機械的に照合して、1件も落ちていないことを確かめた。実際1件だけ言い換えで消えていたので戻している。目視だと絶対に見落とす数だった。
参照切れもテストで縛る
同じテストファイルに、こういうものも足した。
CLAUDE.mdが案内しているスラッシュコマンドが、実際に.claude/commands/に存在するかsettings.jsonが参照しているフックのスクリプトが、実際に存在するか- 根拠ドキュメント側に増えた節が、
CLAUDE.mdからリンクされているか
どれも切れたところで何も起きない参照だ。存在しないコマンドを案内されたエージェントは、黙って手作業に戻る。リンクの無い節は誰にも読まれず、規則だけが独り歩きする。フックのパスが切れれば、規則が効かない状態にそっと戻る。
AI 向けの設定は、壊れても例外が飛ばない。そこが普通のコードと決定的に違う。
フックが一度も動いていなかった
その「効かない状態にそっと戻る」を地で行っていたのがこれ。保存時に prettier をかける PostToolUse フックが、jq で stdin の JSON を読み、pnpm dlx prettier で整形し、末尾を || true で締める作りになっていた。
ところがこのマシンには jq が入っていない。pnpm のランチャも壊れている。つまりこのフックは、設定された時から一度も整形していなかった可能性が高い。|| true が失敗を握りつぶし、実行時のステータス表示だけ毎回出るので、動いているように見えていた。
もっと悪いことに、完了前チェックのコマンド側には「フックが常に整形するので format はスコープ外」と書いてあった。整形を担保する主体が、誰もいない状態になっていた。
直したのは3点。
- フックはリポジトリの依存と Node だけで完結させる。stdin の JSON は
jqではなく Node で読む || trueを外す。PostToolUse の exit 1 は非ブロッキング(ツール呼び出しは成功のまま)で stderr がユーザーに見えるので、黙って無効化されるより exit 1 のほうがいい- 「別の仕組みが担保している」と書くときは、その仕組みが実際に動くことを確認してから書く
直した直後、既存の CLAUDE.md のテーブルまで再整形された。それだけ長いあいだ効いていなかったという証拠でもある。
置き場所を役割で分ける
もう1つ効いたのが、書く場所を分けたことだった。1ファイルに全部書くと、更新すべき箇所が分からなくなって放置される。
| 置き場所 | 何を書くか |
|---|---|
docs/rules/ |
コードから読み取れない意図、外すと壊れる制約、過去に間違えた判断 |
docs/knowledge/ |
再現性のある落とし穴(外部サイトが403になる、この方法は空振りする等) |
| エージェントのメモリ | マシン固有のこと。pnpm が壊れている、といった類 |
そのうえで規約側の第一原則を「このリポジトリを読めば分かることは書かない」にした。一般的な TypeScript や React の作法も書かない。これだけで量は勝手に減る。
ここで1つ実感がある。新しいファイルを作るのは最後の手段にしたほうがいい。知見が出るたびにファイルを増やすと、どれも読まれなくなる。すでに実際に参照されている置き場所に追記するほうが、はるかに生き残る。
余談だが、規約を .agent/ という git submodule に置いていた時期があり、参照先リモートが消えて submodule ごと除去された結果、中身が丸ごと失われた。バックアップは無かった。しかも .gitignore には .agent/ が残っているので、同じ場所に書き直せばまた消える。gitignore されたディレクトリに規約を置かない、というのは笑い話に見えて普通に踏む。
次の一歩
自分の CLAUDE.md を疑うなら、順番はこれがいい。
- まずバイト数を測る。
wc -c CLAUDE.mdだけでいい。想像より大きいはずだ - その数字を上限としてテストに書く。逃がし先を assert メッセージに添える
CLAUDE.mdが案内しているコマンドとフックのパスが実在するか assert する- 最後に、規約の中で一番「監査させたら大量に書き換わりそうな」ルールを1つ選び、実際にそのルールが現行コードで守られているか数えてみる
4番目で1件でも食い違いが出たら、他も疑ったほうがいい。自分は3件出た。
規約を書くところまでは皆やる。書いた規約が実装から剥がれていくのを検出する仕組みは、テストと違って誰も用意してくれない。AI に読ませるなら、そこまでがセットになる。