はじめに
Claude Code を長時間使っていて、「最初は調子よかったのに、会話が進むにつれて応答が雑になってきた」と感じた経験はないでしょうか。
- 直前に依頼した内容をうっかり忘れる
- 既に合意済みの設計方針と矛盾するコードを書き始める
- ファイルを読まずに存在しない関数を呼び出す
- ユーザー指示より「最初のほうで与えられた前提」を優先してしまう
こうした挙動は、モデル自体の性能低下ではなく、Claude Code が内部で行う オートコンパクティング(auto-compact) が一因と考えられます。Zenn でも「Claude Code がアホになるのはあなたのせいじゃない」という記事が話題になりました(参考)。
本記事の射程は「会話が長くなったときの挙動劣化」のみです。2026年3月に話題になった Adaptive Thinking バグ(推論トークンがゼロ割り当てになる問題)とは別の話題として扱います。両者は独立に対策が必要、という見方もあります。
本稿では、auto-compact の仕組みを整理した上で、コンテキスト設計の観点から「長い会話でも挙動を保つ」ための5階層の対策を紹介します。筆者が日々運用している個人プロジェクトのリポジトリでの実例も交えながら、明日から真似できる粒度で書いていきます。
auto-compact とは何か
仕組みの概要
Claude Code は会話履歴が一定量を超えると、古い対話を要約してコンテキストから外す処理を自動で行います。これが auto-compact です。公式ドキュメントでは PreCompact という hook イベントが用意されており、この処理の前後に介入できる点からも、処理の存在が確認できます。
大まかな流れは次のようになります。
PreCompact hook の block は常に安全とは限りません。公式ドキュメントによれば、auto-compact のうち「context-limit 到達後の回復 compaction」をブロックすると request 自体が失敗 します。ブロックは「まだ余裕のある短い会話の段階で早すぎる圧縮を止める」用途に限定するのが安全です。
しきい値の正確な値は公開されていませんが、プランやモデルの context window に応じて可変である、という見方もあります。Opus 4.7 で 1M トークンが標準になったことで状況は改善されつつあります(参考)が、発火自体がゼロになるわけではない、と考えられます。
何が失われるか
要約処理は可逆ではありません。そのため、次のような情報が落ちやすい傾向があります。
| カテゴリ | 失われがちな情報 | 影響 |
|---|---|---|
| ユーザー指示 | 序盤に口頭で伝えた前提・制約 | 禁止していたはずの手法を使い始める |
| コード文脈 | 読んだファイルの具体的な行・関数名 | 存在しない関数・変数を前提に書く |
| 設計判断 | 途中で合意した方針・却下した案 | 再び同じ議論を繰り返す |
| 作業履歴 | 実行したコマンド・テスト結果 | 既に検証済みの問題を再検証し始める |
| 外部参照 | 貼り付けた URL・エラーメッセージ原文 | 曖昧な記憶で誤った推論をする |
特に「一度却下した設計案」を忘れて同じ提案を繰り返される現象は、ユーザー側の体験を大きく損なう原因になりやすい、と考えられます。
auto-compact は「悪」ではありません。これがなければ context window を突き抜けて会話が停止します。問題は「何をどう残すか」をモデル任せにしている部分にあります。
対策の5階層
コンテキスト保全はひとつの手法で解決するものではなく、複数の層を組み合わせる設計が現実的です。ここでは筆者が運用で有効だと感じている5階層を紹介します。
| 階層 | 対策 | 目的 |
|---|---|---|
| 1 |
/clear 運用 |
タスク単位で履歴をリセット |
| 2 | PreCompact hook | 圧縮そのものをブロック |
| 3 | 外部メモリ | 消えても良いようファイル化 |
| 4 | CLAUDE.md 分割 | 必須情報を構造化して毎回注入 |
| 5 | Plan 再開可能化 | 途中成果をファイルに残す |
以下、それぞれを順に見ていきます。
階層1. /clear 運用 ─ タスク単位の区切り
もっとも基本的な対策は、タスクが変わるたびに /clear で会話履歴を明示的にリセットすることです。auto-compact を「発火させないこと」が一番確実、という考え方に基づきます。
運用の目安として、次のような単位で区切ると管理しやすいと感じています。
- 機能実装が1つ完了したタイミング
- 調査フェーズから実装フェーズへの切り替わり
- 午前と午後の境目(集中ブロック単位)
- PR を1本切ったあと
/clear 前には成果物をファイル化しておくことが大切です。次に続く階層3〜5の下準備と考えてください。
階層2. PreCompact hook ─ 圧縮の阻止
/clear での手動リセットを徹底しきれない場面もあります。そのときは PreCompact hook で「意図しないタイミングの圧縮」を機械的に止める選択肢があります。
Hook の契約は次の2パターンです。
-
exit code 2 で終了する ─ 非ゼロ終了のうち
2が特別扱いで、圧縮をブロック(ただし context-limit 到達時のブロックは request 失敗に繋がる) -
stdout に JSON で
{"decision":"block"}を返す ─ 明示的にブロック指定。top-leveldecisionは"block"のみが公式仕様で、許可時はexit 0(decisionを出さない)または単にdecision省略で表現する
公式の PreCompact 入力フィールドは trigger("manual" / "auto")と custom_instructions のみです。session_start_at や mode といったフィールドは存在しないため、時間ベースや Plan mode ベースの判定を行う場合は transcript_path の mtime や外部 state ファイルで代替する必要があります。
実装例を見ていきましょう。
シェルスクリプト例(JSON 返却型)
#!/usr/bin/env bash
# .claude/hooks/pre-compact-guard.sh
# 短すぎるセッションでの auto-compact をブロックする例
# PreCompact 入力には session_start_at は存在しないため、
# transcript ファイルの mtime で経過時間を近似する
set -euo pipefail
# 入力: JSON が stdin で渡される
INPUT="$(cat)"
# 公式入力フィールド: trigger と transcript_path
TRIGGER=$(echo "$INPUT" | jq -r '.trigger // "auto"')
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // empty')
# manual /compact はユーザー意図なのでブロックしない
if [[ "$TRIGGER" == "manual" ]]; then
exit 0
fi
if [[ -z "$TRANSCRIPT" || ! -f "$TRANSCRIPT" ]]; then
# transcript_path が取れないときは何もしない(許可)
exit 0
fi
# transcript の最終更新時刻から経過秒を近似算出
# (session開始からの正確な秒ではないが、外部stateを持たない簡易版)
NOW=$(date -u +%s)
MTIME=$(stat -f %m "$TRANSCRIPT" 2>/dev/null || stat -c %Y "$TRANSCRIPT")
ELAPSED=$((NOW - MTIME))
# 直近更新から短時間で圧縮が走る場合はブロック(早すぎる圧縮を防ぐ)
# 注: context-limit 到達時にブロックすると request 自体が失敗するため、
# 「短い会話のとき限定で止める」設計にしている
if (( ELAPSED < 60 )); then
jq -n --arg reason "Recent activity (${ELAPSED}s). Skipping early auto-compact." \
'{decision:"block", reason:$reason}'
exit 0
fi
# 許可時は decision を出さず exit 0 のみ
exit 0
設定は .claude/settings.json 側で次のように登録する形が一般的です。
{
"hooks": {
"PreCompact": [
{
"matcher": "auto",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/pre-compact-guard.sh"
}
]
}
]
}
}
matcher は PreCompact 入力の trigger に対応します。"auto" を指定するとオートコンパクティングだけが対象になり、ユーザー起動の manual(/compact コマンド)を巻き込みません。"" や "*" は両方にマッチしてしまうため、ユーザー意図の /compact が意図せず阻害される点に注意が必要です。両方を扱いたい場合は "manual" / "auto" を個別に書き分けます。
無条件でブロックし続けると context-limit 到達時の回復 compaction も止めてしまい、request 自体が失敗します(公式明記の副作用)。「短すぎる圧縮だけ止める」「特定の作業中だけ止める」など、条件付きで止める設計をお勧めします。
exit code 2 でブロックする例
最小構成なら exit code だけでも十分機能します。PreCompact 入力には mode や permission_mode フィールドは含まれないため、Plan 中かどうかの判定には外部 state ファイルを使うのが現実的です。
#!/usr/bin/env bash
# .claude/hooks/pre-compact-block-when-planning.sh
# 外部 state ファイルで「作業中フラグ」が立っているときだけブロックする例
# (事前に Plan 開始時点で touch .claude/state/planning しておく想定)
STATE_FILE=".claude/state/planning"
# 公式入力フィールドを参照(mode は存在しないので使わない)
INPUT="$(cat)"
TRIGGER=$(echo "$INPUT" | jq -r '.trigger // "auto"')
# manual はユーザー意図として尊重
if [[ "$TRIGGER" == "manual" ]]; then
exit 0
fi
if [[ -f "$STATE_FILE" ]]; then
echo "Planning session in progress, skip auto-compact." >&2
exit 2
fi
exit 0
stderr に理由を書いておくと、後から debug log(~/.claude/debug/ 配下)でトラブルシュートしやすくなります。
階層3. 外部メモリ ─ 消えても困らない状態を作る
会話に残すのではなく、ファイルに出す。これが一番丈夫な設計、という見方もあります。筆者の場合はプロジェクトルートに docs/memories/ のような専用ディレクトリを切って、以下のようなファイルを置いています(名称や配置は好みで問題ありません)。
docs/memories/
├─ preferences.md # 文体の好み、常用ツール
├─ decisions.md # 意思決定ログ(背景・決定・根拠)
├─ context-log.md # 進行中プロジェクト、検証結果
└─ case-judgment-framework.md # 判断基準・過去パターン
重要な合意事項は「覚えておいて」と依頼して、自作の agent-memory 的なスキル経由で該当ファイルに追記する運用にしています。会話履歴が消えても、ファイルに残っている限り次のセッションで復元できる、という設計です。
階層4. CLAUDE.md の分割 ─ 3階層構造
CLAUDE.md は起動時に必ず読み込まれるため、「auto-compact で消えても再投入される唯一の場所」と考えると扱い方が変わります。肥大化を避けつつ情報密度を上げるために、次の3階層で分けるのが扱いやすいと感じています。
| 層 | 置き場所 | 内容 |
|---|---|---|
| プロジェクト層 |
CLAUDE.md(ルート) |
ディレクトリ構造、スキル発火条件 |
| ルール層 | .claude/rules/*.md |
コーディング規約、Codex活用ルール等 |
| メモリ層 | docs/memories/*.md |
好み、決定、進行中プロジェクト |
ルートの CLAUDE.md から必要な .md を参照させる形にすると、単一ファイルが肥大化せずに済みます。200行以内が目安、という声もコミュニティでよく聞かれます。
階層5. Plan 再開可能化 ─ 中断に強い設計
長いタスクを1つの会話で走り切るのではなく、「途中経過をファイルに残しながら進める」ことで、auto-compact が走っても次セッションで再開できるようにする設計です。
具体的には次のような役割分担にしています。
-
tasks/todo.md─ チェックボックス付きの計画書 -
tasks/lessons.md─ セッション中に得た学び -
ideas/YYYYMMDD-topic.md─ ブレスト・構成の中間成果
ファイルに一度落としてしまえば、「このファイルを読んで続きをお願いします」だけで文脈を再構築できます。auto-compact が発火しても、最悪の場合でも数分のロスで復帰できる、という状態を目標にしています。
実践的なコンテキスト保全パターン
ここまでの5階層を実際のプロジェクト運用に当てはめると、次のようなフローになります。
ポイントは、「compact に至る前に意図的に /clear する」「/clear 前に必ず成果物をファイル化する」という2点です。Hook は保険、ファイル化が本命、と捉えるバランス感覚が現実的だと感じています。
検証方法
対策の効果を測るには、before/after を比較できる手順を踏むのが確実です。以下は簡易的な検証手順の一例です。
-
トークン消費量の可視化
-
/costコマンドやclaude --printの出力を使い、同じタスクを対策前と対策後で実行する - 具体的には、同一リポジトリの同一バグ修正タスクを、
/clear運用なし vs/clear+ memory 運用で計測
-
-
品質の観察項目
- 序盤に指示した制約(例: 「fetch ではなく axios を使う」)が終盤まで守られているか
- 既に読んだファイルを再度読み直していないか
- 存在しない関数・API を捏造していないか
-
ログでの確認
- debug log は
~/.claude/debug/配下、セッション transcript は~/.claude/projects/*.jsonlに出力されるため、ここを見て auto-compact 発火の有無を確認する(.claude/logs/は公式には存在しません) - PreCompact hook を使っている場合は、hook 実行時刻を出力しておくと追いやすい
- debug log は
厳密な定量比較は難しいですが、「存在しない関数を呼んだ回数」「同じファイルを読み直した回数」など半定量な指標を決めると、主観的な印象に流されにくくなります。
注意点
対策を講じても、次のようなモデル側の挙動変化は残ることがあります。
-
モデル自体の微調整による挙動変化
2026年3月の Adaptive Thinking バグのように、バックエンド側の変更が体感を左右するケースがあります(Fortune 報道)。コンテキスト設計だけでは吸収しきれません。 -
PreCompact hook の副作用
無条件でブロックするとコンテキスト上限に当たって会話が停止する、という副作用があります。条件付きブロックとの併用が望ましい、という見方もあります。 -
外部メモリの整合性
memory ファイルと会話内の発言が食い違うと、モデルがどちらを優先するかでブレが出ます。ユーザー側から「memory を真とする」ことを明示しておくと揺れが減る、と感じています。 -
Opus 4.7 での状況改善
1M context window の標準提供により、auto-compact 発火頻度は下がる、という期待があります(公式)。ただしゼロにはならない、と考えるのが安全です。
まとめ
本稿で扱ったポイントを最後にまとめます。
- Claude Code の「会話後半で挙動が雑になる」現象は、auto-compact による文脈要約が一因と考えられる
- 対策は単一の手法ではなく、
/clear運用、PreCompact hook、外部メモリ、CLAUDE.md 分割、Plan 再開可能化の5階層を組み合わせる - Hook は保険として機能させ、本命は「成果物をファイルに残す」設計
- 対策してもモデル側の挙動変化は残るため、定期的な検証と記録が欠かせない
長い会話を一本道で走り切る発想から、「いつでも中断・再開できる設計」へと切り替えると、auto-compact との付き合い方がだいぶ楽になる、という見方もあります。明日から試せるところとして、まず /clear 運用と memory ディレクトリの導入から始めてみるのはいかがでしょうか。