🧠

Claude Codeに「記憶」を持たせる — CLAUDE.md肥大化から抜け出す二層設計

に公開
4
1

はじめに

Claude Code のようなAIコーディングエージェントを数ヶ月使っていると、ほぼ確実に同じ壁に当たります。

  • セッションが変わるたびに、同じ背景説明を繰り返している
  • 「前回決めたはずのこと」をエージェントが覚えておらず、蒸し返される
  • 対策として CLAUDE.md に書き足し続けた結果、数百行に肥大化し、しかも守られない指示が増えてきた

本記事は、この状態から抜け出すための記憶設計を体系的に扱います。特定ツールの操作Tipsではなく、「エージェントの記憶とは何か」という構造から始めて、実装可能な設計パターンまで下ろします。例は Claude Code 中心ですが、考え方は CLI 型エージェント全般に適用できます。

筆者は業務・個人開発でAIコーディングエージェントを日常的に長期運用しており、複数のCLI型エージェントを併用するなかで、ここで書く記憶の仕組みを設計・運用しています。本記事の内容は、その実運用から一般化したものです。

対象読者: Claude Code 等を数ヶ月使い、CLAUDE.md(またはそれに相当する指示ファイル)が伸び続けている人。

この記事で得られるもの:

  1. 記憶を「常駐」と「検索型」の二層に分ける設計原則
  2. CLAUDE.md に書いてよいもの・書いてはいけないものの判定基準
  3. 追記型JSONLによる検索型記憶の最小実装
  4. 古い記憶を安全に無効化する supersede 方式
  5. セッション開始時にダイジェストを自動注入するフックの実装

1. なぜエージェントは「忘れる」のか

まず構造を正確に押さえます。LLM は本質的にステートレスです。エージェントが「覚えている」ように見えるのは、コンテキストウィンドウ(1回の推論に渡されるテキスト全体)の中に情報が入っているからにすぎません。

コンテキストウィンドウは作業机だと考えてください。

  • セッション中の会話・読んだファイル・ツール実行結果は、すべて机の上に置かれた紙
  • セッションが終わると、机の上の紙は全部捨てられる
  • 次のセッションで机に最初から載っているのは、CLAUDE.md などの「毎回自動で置かれる紙」だけ

つまり「エージェントに記憶を持たせる」とは、モデルに何かを覚えさせることではなく、次のセッションの机に、どの紙を・どうやって載せるかを設計することです。記憶設計とは注入戦略の設計である——これが本記事全体を貫く視点です。

この視点に立つと、記憶に関する問題は2つに分解できます。

  1. 何を残すか: セッション中に生まれた情報のうち、将来価値があるものをどう選別・保存するか
  2. どう戻すか: 保存した情報を、次のセッションのコンテキストへどのタイミング・どの分量で注入するか

CLAUDE.md 肥大化は、この2つを区別せず「残す=全部CLAUDE.mdへ」「戻す=全文を毎回注入」という最も素朴な戦略を取った結果として起きます。

2. 「とりあえずCLAUDE.mdに書く」がなぜ破綻するか

CLAUDE.md への追記は、対症療法としては正しく機能します。問題は構造にあります。

2.1 書くコストと消すコストの非対称

追記は一瞬です。「これも覚えておいてほしい」と思った瞬間に1行足せば済む。一方、削除には「この指示はもう不要だ」という判断が要ります。不要かどうかは過去の経緯を思い出さないと判定できないので、認知コストが高い。結果、流入だけあって流出のないダムになり、水位は上がり続けます。

2.2 全文が毎セッション無条件に注入される

CLAUDE.md は毎セッション、コンテキストの先頭近くに全文注入されます。500行のCLAUDE.mdは、今日の作業に関係あるかどうかにかかわらず毎回数千トークンを消費します。これは単なる料金の問題ではありません。コンテキストには予算があり、指示で埋めた分だけ、コードや会話に使える領域が減ります。

2.3 指示は増えるほど守られなくなる

経験的に最も痛いのがこれです。指示が10個のときは全部守られていたのに、100個になると個々の遵守率が目に見えて下がります。LLMへの指示は「多いほど安全」ではなく、重要な指示ほど、少ない指示の中に置いたほうが守られる。肥大化したCLAUDE.mdは、いちばん守ってほしい規範の遵守率を自ら下げているのです。

2.4 古い記述が「正しい顔」をして残る

3ヶ月前に書いた「テストは make test で実行」が、ビルドシステム移行後も残っている——ファイル内の記述には鮮度のメタデータがないため、エージェントは古い情報を現役の指示として扱います。間違った記憶は、記憶がないことより悪い

3. 中核原則: 常駐記憶と検索型記憶を分離する

解決の中核はシンプルです。人間の組織が「全員が毎朝読む行動規範」と「必要なときに引く議事録・ナレッジベース」を分けているのと同じ分離を、エージェントにも適用します。

常駐記憶 検索型記憶
実体 CLAUDE.md 等の自動注入ファイル 追記型のログ/ノート(後述: JSONL)
注入 毎セッション・全文・無条件 必要時にエージェント自身が検索して読む
内容 行動規範・制約・承認境界 決定の記録・知見・経緯・失敗事例
量の上限 厳しく絞る(目安: 100行前後) 事実上無制限(検索されるだけ)
鮮度管理 常に現在形に保つ(履歴を持たない) 追記のみ+supersedeで無効化(履歴が正本)

振り分けの判定基準は1つで足ります。

「この情報は、毎セッション必ず必要か?」

Yes → 常駐記憶。ただし後述の記述規律に従う
No → 検索型記憶。CLAUDE.md には書かない

「毎セッション必要」のハードルは想像より高いです。たとえば「認証モジュールのリファクタで◯◯という設計判断をした」は重要な情報ですが、毎セッション必要ではありません。認証を触るセッションでだけ必要です。これは検索型に置き、必要なセッションで引かせます。

この分離を入れた瞬間に、CLAUDE.md 肥大化の圧力は消えます。「覚えておいてほしいこと」の受け皿が別にできるので、CLAUDE.md は「守ってほしいこと」だけの短いファイルに戻せます。

4. 常駐記憶(CLAUDE.md)の設計

4.1 書いてよいもの

  • 行動規範: コードスタイル、コミット規約、応答言語など、全作業に横断的にかかるルール
  • 承認境界: 「◯◯は必ず人間の承認を取る」「△△は自走してよい」。安全に関わるため常駐が正当化される筆頭
  • プロジェクト固有の非自明な制約: コードを読んでも分からない事情(「このディレクトリは自動生成なので編集禁止」「デプロイは必ず staging を経由」)
  • 頻用コマンド: テスト・ビルド・lint の実行方法(毎セッション使うため)
  • 他の記憶への入口: 「過去の決定は◯◯を検索せよ」というポインタ(後述)

4.2 書いてはいけないもの(アンチパターン)

(a) 作業ログ・会話の要約の貼り付け
「前回はここまでやった」は次の1セッションにしか価値がないのに、永続ファイルに入れると腐って残ります。作業状態は検索型記憶(セッションサマリー)へ。

(b) コードやリポジトリから再導出できる事実
「このプロジェクトはTypeScriptでExpressを使い…」はコードを読めば分かります。エージェントは読むのが速い。書くだけ二重管理になり、コード変更で嘘になります。

(c) 一時的なTODO・進行中タスクの状態
タスク状態は専用の仕組み(イシュートラッカー、タスクファイル)に置き、CLAUDE.md には持ち込まない。

(d) 矛盾した指示の放置
追記を重ねると「PRを作る前に必ずレビュー依頼」と「小さな修正は直接mainへ」のような衝突が生まれます。エージェントは矛盾に直面すると、どちらかを無言で選びます。つまり動作が非決定的になる。定期的に通読して矛盾を潰す(後述のキュレーション)。

4.3 それでも常駐が増えるとき: オンデマンド参照方式

規範だけに絞っても、UIガイドライン・レビュー観点・特定作業の手順書など「特定の場面でだけ必要な長文」は存在します。これらは本文を常駐させず、参照表だけを常駐させます

## 作業別リファレンス(該当作業を始めるとき対応ファイルを読むこと)

| トリガー | 参照先 |
|------|-------|
| UI実装に着手 | docs/guides/design-system.md |
| DBマイグレーション | docs/guides/migration.md |
| リリース作業 | docs/guides/release.md |

常駐コストは表の数行だけで、本文数百行はエージェントが該当作業のときにだけ読みます。CLAUDE.md の @path/to/file インポート構文は便利ですが、インポートは常駐注入なので、「毎回は要らない長文」にはこの参照表方式のほうが適します。

5. 検索型記憶の設計: 追記型JSONL

検索型記憶の実装は、凝ったものは要りません。1行1レコードの追記型JSONLファイルで十分です。

{"id":"m-0412","ts":"2026-06-02T10:30:00+09:00","type":"decision","content":"認証はセッショントークン方式を採用。JWT案は失効管理の複雑さで不採用","tags":["auth","architecture"]}
{"id":"m-0413","ts":"2026-06-03T15:12:00+09:00","type":"lesson","content":"E2Eテストはローカルとサンドボックスでタイムゾーンが異なり日付跨ぎで落ちる。TZ固定で実行する","tags":["test","ci"]}
{"id":"m-0414","ts":"2026-06-05T09:00:00+09:00","type":"summary","content":"セッション要約: 決済モジュールのリトライ設計を確定し実装着手。残タスクは冪等キーの永続化","tags":["payment"]}

5.1 スキーマは4フィールド+αで始める

  • id: 一意ID(後述の supersede で参照するため必須)
  • ts: タイムスタンプ(鮮度判定の根拠)
  • type: 最低限 decision(決定)/ knowledge(知見) / lesson(失敗からの教訓)/ summary(セッション要約)の4種
  • content: 本文。1レコード=1事実に保つ(複数の事実を1行に詰めると、検索でヒットしたとき無関係な情報まで注入される)
  • tags: 検索の補助(任意)

5.2 なぜ JSONL か

  • 追記が安全: 既存行を書き換えないので、並行アクセスや誤編集で過去が壊れない
  • diffが読める: Gitに載せれば記憶の変化自体をレビューできる
  • ツール非依存: 特定のエージェント製品の独自機能に記憶を預けると、ツールを乗り換えた瞬間に記憶を失います。正本をプレーンテキストで持ち、各ツールへは注入で渡す構成にすると、記憶がポータブルになる。これは製品の記憶機能(Claude Code の auto-memory など)を使うなという意味ではなく、正本をどこに置くかという話です。製品側の常駐記憶には「以後の意思決定に本当に効く少数の事実」だけを置き、正本はJSONLに集約する、という役割分担が安定します

5.3 検索は grep で始めよ

「記憶検索」と聞くとベクトルDBやembeddingを連想しますが、数千レコード規模までは grep で足ります。理由は、検索の主体が人間ではなくエージェントだからです。

人間向け検索は1発で当てる必要がありますが、エージェントは「auth で検索 → ヒットが多すぎる → auth token に絞る → 類義語 session でも引く」という反復検索を自分で回せます。表記揺れはクエリ側の言い換えで吸収できるので、インフラ側の意味検索は必須ではありません。まず grep で運用を始め、レコード数が万を超えて反復検索でも捕まらなくなったときに初めて embedding を検討する、が正しい順序です。

5.4 いつ保存するか

保存タイミングを人間の「覚えておいて」発言に依存させると、記憶は穴だらけになります。定型のタイミングを決めます。

  1. 設計判断・方針決定の直後(type: decision)— 最重要。決定は会話の中に埋まると二度と発掘されない
  2. 失敗から回復した直後(type: lesson)— 同じ失敗の再発防止に直結
  3. セッション終了時(type: summary)— 次セッションの「前回までのあらすじ」になる

この3つをエージェント自身の手順(カスタムコマンドやスキル)にしておくと、記憶が自動的に貯まります。

6. 鮮度管理: supersede 方式

検索型記憶の最大の敵は、古い決定が正しい顔をして検索にヒットすることです。「認証はJWT」と「認証はセッショントークン」が両方ヒットしたら、エージェントはどちらかを信じます。

対策として、過去行の書き換え・削除を禁止し、無効化レコードの追記で上書きします

{"id":"m-0301","ts":"2026-04-10T11:00:00+09:00","type":"decision","content":"認証はJWTを採用","tags":["auth"]}
{"id":"m-0412","ts":"2026-06-02T10:30:00+09:00","type":"decision","content":"認証はセッショントークン方式に変更。JWTは失効管理の複雑さで撤回","tags":["auth"],"supersedes":["m-0301"]}
  • 新レコードの supersedes に旧レコードのIDを列挙する
  • 検索・注入時には「他レコードの supersedes に含まれるIDのレコード」を除外(または打ち消し済みと明示)する
  • 削除しないので、「なぜJWTをやめたのか」という経緯も検索可能なまま残る

運用ルールとしては1行で表せます: 決定を覆すときは、新決定の保存と旧決定の supersede をセットで行う。これを怠ると、覆したはずの決定がゾンビとして検索に湧き続けます。

定期キュレーション

supersede があっても、月に1度程度の棚卸しは必要です。やることは3つ。

  1. 常駐記憶(CLAUDE.md)を通読し、古くなった記述・矛盾を検索型へ退避または削除
  2. 製品側の常駐記憶(auto-memory 等)に溜まった事実のうち、生きているものをJSONL正本へ還流
  3. supersede されたレコード比率や総件数を眺め、検索ノイズが増えていないか確認

「常駐は現在形に保つ・検索型は履歴を正本とする」という役割の違いを、キュレーションで維持します。

7. セッション開始時の自動注入

検索型記憶には弱点があります。エージェントが検索しようと思わなければ、存在しないのと同じです。これを塞ぐのがセッション開始時の自動注入で、Claude Code なら hooks の SessionStart が使えます。

設計原則は「全文ではなくダイジェスト+ポインタ」。よくやりがちな「記憶ファイル全体を注入」は、CLAUDE.md 肥大化を場所を変えて再演するだけです。

// .claude/settings.json(抜粋)
{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          { "type": "command", "command": ".claude/hooks/memory-digest.sh" }
        ]
      }
    ]
  }
}
#!/usr/bin/env bash
# .claude/hooks/memory-digest.sh — 直近の要約1件+最近の決定3件だけを注入する
MEM="memory/log.jsonl"
[ -f "$MEM" ] || exit 0

echo "[記憶ダイジェスト] 詳細は $MEM を grep で検索すること"
# 最新のセッション要約1件
grep '"type":"summary"' "$MEM" | tail -1 | jq -r '"- 前回要約(\(.ts|split("T")[0])): \(.content)"'
# 最近の決定3件(supersede済み除外は簡略化のため省略)
grep '"type":"decision"' "$MEM" | tail -3 | jq -r '"- 決定(\(.ts|split("T")[0])): \(.content)"'

ポイントは3つです。

  1. 件数上限を固定する(要約1件+決定3件など)。記憶が増えても注入コストが一定になる
  2. ダイジェストに「続きの引き方」を含める。「詳細は◯◯を検索」という1行が、エージェントに検索型記憶の存在を毎回思い出させる
  3. 出力は事実の羅列にする。ここに指示を混ぜ始めると第二のCLAUDE.mdになる

この仕組みが入ると、セッション開始直後のエージェントは「前回までのあらすじ+最近の決定+記憶の在り処」を持った状態で立ち上がります。体感が最も変わるのはここです。

8. 何を保存するか: 「将来の判断に効く事実」だけ

仕組みができると、今度は保存しすぎが新しい敵になります。何でも保存すると検索ノイズが増え、ヒットしても信用できない記憶ベースになります。保存前に3つの問いを通します。

  1. 次のセッションの判断が変わるか? — 変わらない情報(単なる作業ログ、感想)は保存しない
  2. リポジトリから再導出できないか? — コード・Git履歴・既存docsにある事実は保存しない。記憶は「どこにも書かれていないこと」の受け皿
  3. 1ヶ月後も真か? — 真でなくなる予定の情報(進行中タスクの途中経過など)は、summaryとしてなら可、decisionとしては保存しない

もう1つの規律が二重書きの禁止です。同じ事実を CLAUDE.md と JSONL の両方に書くと、片方だけ更新されて食い違ったとき、エージェントはどちらを信じるべきか判断できません。事実には正本を1つだけ定め、他方には書かない(書くならポインタのみ)。

9. 最小実装まとめ(コピペ用)

ここまでの設計は、シェルスクリプト2本とフック1本で始められます。

#!/usr/bin/env bash
# memory-add.sh <type> <content> [supersedes-id]
# 例: ./memory-add.sh decision "認証はセッショントークン方式を採用" m-0301
MEM="memory/log.jsonl"
mkdir -p "$(dirname "$MEM")"
ID="m-$(date +%s)"
jq -nc --arg id "$ID" --arg ts "$(date -Iseconds)" \
      --arg type "$1" --arg content "$2" --arg sup "${3:-}" \
  '{id:$id, ts:$ts, type:$type, content:$content}
   + (if $sup != "" then {supersedes:[$sup]} else {} end)' >> "$MEM"
echo "saved: $ID"
#!/usr/bin/env bash
# memory-search.sh <keyword> — supersede済みを除外して検索
MEM="memory/log.jsonl"
SUPERSEDED=$(jq -r 'select(.supersedes) | .supersedes[]' "$MEM" | sort -u)
grep -i "$1" "$MEM" | jq -r --arg dead "$SUPERSEDED" \
  'select(.id as $i | ($dead | split("\n") | index($i)) | not)
   | "[\(.ts|split("T")[0])][\(.type)] \(.content)"'

これに前節の memory-digest.sh を SessionStart フックとして加えれば、二層構成の骨格は完成です。CLAUDE.md には次の数行を足します。

## 記憶
- 過去の決定・知見は `memory/log.jsonl` が正本。着手前に `./memory-search.sh <キーワード>` で検索する
- 設計判断をしたら直後に `./memory-add.sh decision "..."` で保存する。決定を覆すときは旧IDを supersede する
- セッション終了時に summary を保存する

エージェント製品のカスタムコマンド/スキル機能がある場合は、この保存・検索手順をコマンド化しておくと運用が安定します。

10. アンチパターン早見表

アンチパターン 症状 対策(本記事の節)
何でもCLAUDE.mdに追記 肥大化・遵守率低下 常駐/検索型の分離(§3)
作業ログの常駐化 古い「前回の続き」が腐って残る summaryは検索型へ(§4.2, §5.4)
コードで分かることを記憶に書く 二重管理・コード変更で嘘になる 保存前3問(§8)
記憶ファイルの上書き編集 経緯の消失・決定のゾンビ化 追記のみ+supersede(§6)
記憶全文をセッション冒頭に注入 場所を変えた肥大化 ダイジェスト+ポインタ(§7)
保存を「覚えておいて」発言に依存 記憶が穴だらけ 定型タイミングで保存(§5.4)
常駐と検索型への二重書き 食い違い時に信用崩壊 正本は1つ(§8)
最初からベクトルDB インフラ過剰・運用が続かない grepで開始(§5.3)

おわりに

まとめます。

  • エージェントの記憶とは、次セッションのコンテキストへの注入戦略である
  • 記憶は**常駐(規範・少量・毎回注入)検索型(事実・大量・必要時検索)**に分離する
  • 検索型は追記型JSONL+grepで始め、supersedeで鮮度を守る
  • SessionStartフックのダイジェスト注入が、検索型記憶を「思い出せる記憶」に変える
  • 保存基準は「将来の判断に効く事実」だけ

この設計にしてから、筆者の環境では「セッションをまたいだ説明のやり直し」がほぼ消え、CLAUDE.md は規範だけの短いファイルに戻りました。

次回は、この記憶設計と対になるテーマ——サブエージェントへの委譲設計(どの作業を任せてよいか・結果をどう検収するか・暴走をどう防ぐか)を予定しています。

あなたの環境の記憶運用で困っていること、この設計への疑問があれば、ぜひコメントで教えてください。今後の記事の優先度に反映します。

4
1

Discussion

ログインするとコメントできます
4
1