半日かけて、Claude Code とこう決めた。「ユーザーの住所はサーバーに保存しない。クライアント側だけで処理する」。理由も書いた。ADR も残した。満足してセッションを閉じた。
三つほどセッションをまたいだ頃、Claude Code は何食わぬ顔で、サーバー側に住所を保存する実装を書き始める。
殺したはずの設計が、生き返っている。
この現象には、いま名前がついている。context drift(コンテキストドリフト) だ。同じ経験、ありませんか。よく見ると、いつも同じ4つの形をしている。
- 廃案の復活 — 一度却下した案を、現行案として書き直す
- 未決の既決化 — まだ決めていないことを、決まったものとして実装する
- 仕様と実装の取り違え — 「こう動くべき」と「こう書いてある」を混同する
- 古い前提の採用 — もう変わった前提を、まだ生きているものとして使う
そして多くの人が、最初に同じ対策を打つ。「忘れるなら、全部 CLAUDE.md / AGENTS.md に書いておけばいい」と。
これが、実証的に逆効果だという話から始める。
「コンテキストを厚くする」は、データで否定されている
2026年2月、ETH Zürich と LogicStar.ai が、この直感を正面から検証した(arXiv:2602.11988)。公開直後に Hacker News で延焼したので、見た人もいるかもしれない。
彼らは実在リポジトリ由来の138件のタスクで AGENTBENCH を組み、4つのフロンティアモデルを3条件(コンテキストファイルなし/LLM生成あり/開発者手書きあり)で走らせた。
結果はこうだ。コンテキストファイルがある方が、何もない場合よりタスク成功率を下げ、推論コストを20%以上増やす傾向が出た。 開発者が手で書いたファイルでさえ、ほとんど成績を改善しなかった。
「リポジトリの知識を詰め込むほど賢くなる」というベンダー各社の推奨は、少なくとも素朴な形では成り立たない。足すほど下手になり、財布も痛む。
ここで考えを切り替える必要がある。問題は量ではない。
本質:情報に「地位(status)」がない
2024年はプロンプトエンジニアリングの年だった。2026年、その座を奪ったのがコンテキストエンジニアリングだ。中心にある発見はシンプルで、「コンテキストは多いほど良い、ではない。最小で高信号な集合に絞るほど良い」。
では、なぜ Claude Code は廃案を復活させるのか。答えはこうだ。
あなたのリポジトリの中で、現行の仕様も、却下された提案も、未決の論点も、ただの調査メモも、全部「同じ見た目・同じ粒度」で並んでいるから。
人間が後から読んでも、どれが現行で、どれが死んでいるのか分からない。地位(current なのか、廃案なのか、未決なのか)が、文書のどこにも書かれていない。人間に区別できないものは、LLM にも区別できない。 だから LLM は古いログや死んだ提案を拾い、現行として再採用する。
ここで効く視点を一つ。
文書は資産ではなく、負債である。
生成コストはほぼゼロになった。だが、読む・保守する・信頼するコストは人間に残り続ける。やるべきは「たくさん書いて全部読ませる」ではなく、各情報に地位を与えて追跡可能にし、LLM には最小限だけを渡すことだ。
解決は「プラグインを配る」ではなく「設計書を渡す」
ここで普通は「専用プラグインを梱包して配る」に行きたくなる。一度やりかけて、やめた。リポジトリは一つひとつ違う。固い梱包物より、各プロジェクトに合わせて組み上がる"設計書"の方が効く。
これは流行りの言葉でいう スペック駆動開発(spec-driven development) そのものだ。合言葉は "the spec is the prompt"。Kiro や Spec Kit のような専用ツールが無くても、markdown 1枚と手元のエージェントがあれば成立する。価値はツールではなく、設計書を書くときの思考の側にある。
そこで、情報統治の設計書を1枚にまとめて GitHub に置いた。
🧭 https://github.com/Forest-Project-Lab/context-engineering-blueprint
使い方はこれだけ。Claude Code にこう言う:
この設計書をベースに、このリポジトリに合った最小構成を構築して。
既存ファイルは壊さず、まず Level 1(最小5ファイル+薄いルーター)から。
すると、そのプロジェクトの実情に合わせた形で、以下の仕組みが入る。設計書には参照実装も同梱してあるので、Claude はそれを土台にして組み上げる。
設計の核:決定論(Hook)と判断(Skill)を分ける
Claude Code の拡張はレイヤーごとに性質が違う。ここを混ぜないのが肝だ。
| レイヤー | 性質 | 比喩 | 担当 |
|---|---|---|---|
| Hook | 決定論的。毎回必ず走る | リンタ / CI | 不変条件の機械的強制 |
| Skill | 確率的。文脈でモデルが判断 | 専門知識を持つ相棒 | 手続きと判断の支援 |
| CLAUDE.md | 常時投入。毎ターン読まれる | 受付の案内図 | 薄いルーター(知識を貯めない) |
退行(context drift)を「お願いベース」で防ぐのをやめる。決定論で守れるものは Hook が機械的に守り、判断が要るものだけ Skill に任せる。 そして両者は同じ一つの仕様を参照する(規則を二重に持たない)。具体的な手は4つだ。
手① 文書に「地位」と最小メタデータを与える
ステータスは自由語を禁止し、統制された7語だけを使う。自由に増やすと、機械検証も人間の探索性も即崩れる。
| 値 | 意味 |
|---|---|
draft |
草案。まだ現行ではない |
proposed |
提案中。承認待ち |
current |
現行。いまの真実 |
deprecated |
廃案。撤回済み |
superseded |
後継ありで置換された |
archived |
退役。証跡として保管 |
open |
未決。決まっていない |
各文書の先頭に、最小限の YAML を置く。必須はあえて絞る(多すぎると必ず形骸化する)。
---
id: SPEC-014
title: 返金ポリシー
type: SPEC
status: current
owner: yamada # チーム名でなく個人名。腐敗の責任の所在
updated: 2026-06-24
sources: [ADR-0009] # 出所。人間由来かLLM由来か、どの根拠か
depends_on: [REQ-003]
---
手② 保存のたびにリンタを自動で走らせる(PostToolUse Hook)
これが「CI でリンタを回し、メタデータの欠落や死んだ参照をビルドエラーで弾く」の実体だ。Claude が文書を書き換えた直後に、触れたファイルだけを検証する(単一ファイルなので速い)。違反があれば、ターンを止めずに Claude へ指摘を返して直させる。
スクリプトは標準ライブラリだけで書く。pip install が要る時点で「どこでも動く」を失うからだ。本質を抜き出すとこれだけで動く。
#!/usr/bin/env python3
# .claude/scripts/docs-linter.py — PostToolUse Hook が stdin で受け取る
import json, sys, re
STATUS = {"draft","proposed","current","deprecated","superseded","archived","open"}
STATUS_BY_TYPE = {"ADR": {"proposed","accepted","superseded","deprecated"}} # ADRだけ例外
REQUIRED = ["id","title","type","status","owner","updated","sources"]
def front(text):
m = re.match(r"^---\s*\n(.*?)\n---", text, re.S)
meta = {}
if m:
for line in m.group(1).splitlines():
if ":" in line and not line.lstrip().startswith("#"):
k, v = line.split(":", 1)
meta[k.strip()] = v.strip().strip("[]")
return meta
data = json.load(sys.stdin)
path = (data.get("tool_input") or {}).get("file_path", "")
if "/docs/" not in path or not path.endswith(".md") or "/99-archive/" in path:
sys.exit(0) # 対象外。アーカイブは不変なので検証もしない
meta = front(open(path, encoding="utf-8").read())
errs = [f"必須メタデータ欠落: {k}" for k in REQUIRED if not meta.get(k)]
allowed = STATUS_BY_TYPE.get(meta.get("type", ""), STATUS)
if meta.get("status") and meta["status"] not in allowed:
errs.append(f"status '{meta['status']}' は型 {meta.get('type')} で不許可")
if errs:
print(json.dumps({
"decision": "block",
"reason": "ドキュメント・リンタ違反:\n- " + "\n- ".join(errs),
"continueOnBlock": True # ブロックでなく指摘として返し、Claudeに直させる
}))
sys.exit(0)
実際にはここに「参照先IDが実在するか(dead link 検出)」「type とフォルダが整合するか」を足す。continueOnBlock: true がポイントで、Claude を止めるのではなく指摘を tool_result として返す。Claude はそれを読んで自分で修正・再試行する。人間が介入しなくても、品質が一段上がる。
手③ アーカイブと確定ADRを物理的に守る(PreToolUse Hook)
退役した文書(/99-archive)は不変の証跡だ。編集できると、そこから context drift が起きる。ADR も同じで追記型にする。覆すときは古い ADR を消すのではなく、新しい ADR を作って旧 ADR に superseded_by を付ける。これは判断ではなく規律なので、PreToolUse Hook が対象パスへの編集を deny するだけでいい。「過去に戻る」事故が物理的に起きなくなる。
手④ 注入するのは「事実」だけ。廃案の"本文"は渡さない(SessionStart Hook)
ここが一番、間違えやすくて効く。
退行を防ぐために、確定事実・非目標・撤回事項を毎セッション注入したくなる。正しい。だが 撤回した案の「本文」を渡してはいけない。 廃案の中身を読ませると、それ自体が復活の燃料になる。渡すのは「これは撤回された/撤回日/後継はこれ」という事実のラベルだけだ。
そして ETH の論文どおり、ここでも最小を死守する。SessionStart Hook で流すのは、確定事実・非目標・撤回事項(事実のみ)・退行監視リストの要点だけ。本文全量ではなく箇条書きの要点だけを入れる。CLAUDE.md 本体は知識の置き場ではなく、入口へのリンクだけを並べた薄いルーターに保つ。
# CLAUDE.md(薄いルーター)
このリポジトリの情報統治は /docs にある。ここには知識を貯めない。
## まず読む(常時)
- 確定済み事実: docs/90-llm/decided-facts.md
- 非目標(やらないこと): docs/10-product/non-goals.md
- 戻ってはならない事項: docs/70-test/regression-watchlist.md
## 規約
- 現行仕様は docs/20-spec/。決定の経緯は docs/40-decisions/(ADRは追記型)
- docs/50-research/ と docs/99-archive/ の本文は「現行ではない」。現行として扱わない
- 撤回済み・未決を勝手に決定しない。未決は人間に返す
タスク固有の深さは、すべてリンク先に置く。常時投入は確定事実だけ。これで CLAUDE.md 肥大による性能低下とコスト増を避けられる。
正直な話:これは「予防」ではなく「検出」だ
ここまで威勢よく書いたが、効能の範囲を誤魔化さないでおきたい。シェアする価値があるかどうかは、たぶんここで決まる。
構造と Hook で検出・早期発見できるのは、メタデータ欠落、ステータス逸脱、dead link、アーカイブへの編集、廃案の常時不在による退行——このあたりだ。
一方で、構造だけでは閉じないものもはっきりある。仕様が実装どおり動くかの最終検証は受入テスト・退行テストの仕事で、文書は意図を、テストは挙動を保証する(役割が違う)。人間が書き忘れた暗黙のビジネス前提は構造では補えない。LLM の確率的な逸脱も、外部 API・価格・法令の予告なき変更も、予防はできない。「100%の予防」は構造上不可能で、できるのは適切に運用された場合に特定の失敗類型を検出することに限られる。
そして——これが一番大事だが——最大のリスクは技術ではなく運用にある。 メタデータが更新されず監視リストが保守されなくなった瞬間、この体系は「管理できている感」だけを生む置物に変わる。ツールは規律の欠如を直せない。一年後にこれが破綻するとしたら、原因はほぼ「Hook が重すぎて皆が切った」か「メタデータが面倒で誰も書かなくなった」のどちらかだ。だから設計の側でも、リンタは毎ターン単一ファイルだけ検証し、必須メタデータは7項目に絞り、空のフォルダ群は先に作らず初使用時に遅延生成する——避けたかった散乱を、足場の形で先に作らないために。