Claude Media
PreCompact/PostCompact hookでコンテキスト圧縮の前後に介入する

PreCompact/PostCompact hookでコンテキスト圧縮の前後に介入する

PreCompactは圧縮前に発火して自動圧縮を止められ、PostCompactは圧縮後の要約を受け取ります。設定例と、自動圧縮をブロックしたときに起こる分岐を解説します。

PreCompact/PostCompact hookとは

PreCompactはClaude Codeがコンテキストの圧縮(compaction)を実行する直前に、PostCompactは圧縮が完了した直後に発火するhookイベントです。圧縮は会話履歴を要約してコンテキストウィンドウの空きを作る処理で、手動の/compactと、コンテキストウィンドウが埋まったときに自動で走るauto-compactのどちらでも同じ2つのイベントが発火します。PreCompactは圧縮そのものを止められる数少ないイベントの一つで、PostCompactは圧縮結果を変えられない代わりに要約テキストを受け取れます。フックイベント全体の一覧はHooks完全ガイドにまとめてあるので、本記事はこの2つのイベントが受け取る情報と実装の分岐に絞ります。

前提条件

手順1: PreCompactで自動圧縮を止める

PreCompactは共通フィールドに加えてtriggerとcustom_instructionsを受け取ります。triggerは/compactによる手動実行ならmanual、コンテキストウィンドウが埋まったことによる自動実行ならautoです。custom_instructionsは手動実行時にユーザーが/compactに渡した文字列で、渡さなければnull、自動実行では常にnullになります。

{
  "session_id": "abc123",
  "cwd": "/Users/you/project",
  "hook_event_name": "PreCompact",
  "trigger": "manual",
  "custom_instructions": null
}

止め方は2通りです。exit 2で終了するか、JSON出力で"decision": "block"を返すかのどちらかを使います。次のスクリプトは、プロジェクト直下に.claude/no-compactというマーカーファイルがある間だけ自動圧縮を止め、ユーザーが明示的に求めた手動の/compactは常に通します。

#!/bin/bash
# .claude/hooks/guard-compact.sh
input=$(cat)
trigger=$(jq -r '.trigger' <<<"$input")
cwd=$(jq -r '.cwd' <<<"$input")
 
if [[ "$trigger" == "auto" && -f "$cwd/.claude/no-compact" ]]; then
  echo "自動圧縮を一時停止中です(.claude/no-compactが存在)" >&2
  exit 2
fi
 
exit 0

設定側はmatcherをautoに絞ることで、手動の/compactではこのフックを起動させません。triggerの判定だけでも動きますが、絞っておいたほうが手動実行のたびにプロセスを起動するコストを避けられます。matcherを省略するか"*"を指定すると手動・自動の両方で発火するので、両方のケースをまとめて処理したいときはこちらを使います。

{
  "hooks": {
    "PreCompact": [
      {
        "matcher": "auto",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard-compact.sh",
            "args": []
          }
        ]
      }
    ]
  }
}
くらべる

自動圧縮をブロックしたときの結果は発火理由で分かれる

スキップされる

先回りで走った圧縮

コンテキスト上限に達する前の圧縮をブロックすると、圧縮は行われず、会話は要約されないまま続きます。

リクエストが失敗する

エラー回復で走った圧縮

APIがコンテキスト上限エラーをすでに返したあとの圧縮をブロックすると、そのエラーが表に出て現在のリクエストが失敗します。マーカーファイルを置きっぱなしにすると、この経路で会話が止まります。

自動圧縮が走る位置は、モデルとコンテキストウィンドウで変わります。1Mトークンのウィンドウを持つモデルの既定は約967Kトークンで、200Kで区切る構成もあります。/autocompactやautoCompactWindowで窓を縮めている場合は、その値が先回りの発火点になります。

PreCompactが返すsystemMessageとcontinueフィールドはClaude Codeが破棄します。ブロック理由を見せたいときに頼れるのはstderrで、画面に出るのは手動の/compactのときだけです。

ハンドラはtype: "command"以外も選べます。typeをhttpにすればHTTPエンドポイントへ同じJSONをPOSTでき、mcp_toolにすれば接続済みのMCPサーバーのツールを呼べます。どちらも受け取るJSONの形と返すべきdecisionフィールドはcommandと同じです。タイムアウトはcommand・http・mcp_toolのいずれも既定600秒のままで、UserPromptSubmitのように短縮される対象には含まれません。ガード用のスクリプトが重い処理(外部APIへの問い合わせなど)を挟む場合は、この600秒が実質的な上限になります。

手順2: PostCompactで圧縮後の要約を記録する

PostCompactは圧縮完了後に発火し、triggerに加えてcompact_summary(生成された要約テキスト)を受け取ります。圧縮結果を変えることはできないので、外部ログへの記録や監査証跡の更新といった副作用専用のイベントです。

{
  "session_id": "abc123",
  "cwd": "/Users/you/project",
  "hook_event_name": "PostCompact",
  "trigger": "auto",
  "compact_summary": "Summary of the compacted conversation..."
}

次のスクリプトは、発火のたびにtriggerと要約の文字数をJSON Lines形式でログファイルへ追記します。要約の全文ではなく文字数だけを記録しているのは、長いセッションではcompact_summary自体が数千文字に達することがあり、ログファイルが際限なく肥大化しやすいためです。全文が必要な監査要件がある場合はcompact_summaryの値をそのまま書き出してください。

#!/bin/bash
# .claude/hooks/log-compact.sh
input=$(cat)
cwd=$(jq -r '.cwd' <<<"$input")
jq -c '{ts: now, trigger: .trigger, summary_length: (.compact_summary | length)}' <<<"$input" \
  >> "$cwd/.claude/compact-log.jsonl"
exit 0
{
  "hooks": {
    "PostCompact": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/log-compact.sh",
            "args": []
          }
        ]
      }
    ]
  }
}

terminalSequenceはsystemMessageやcontinueを破棄するイベントでも動作する共通フィールドで、公式ドキュメントはNotificationやStopFailureのような破棄対象イベントでの動作例を挙げています。PostCompactも同じくsystemMessageとcontinueを破棄するイベントなので、圧縮完了をユーザーに知らせたいときは、ログ書き込みに加えてterminalSequenceでデスクトップ通知を出せます。対話セッションで画面が出ている間だけ有効で、非対話モードの-pフラグとAgent SDKでは無視されます。

PostCompactは決定制御(decision control)を一切持ちません。exit 2で終了してもstderrがユーザーに表示されるだけで圧縮結果には影響しないので、ログ書き込みが失敗しても圧縮そのものは既に完了しています。リトライや失敗通知が必要なら、スクリプト側で組み込む必要があります。

PreCompact・PostCompact・SessionStartの使い分け

圧縮の前後に絡むhookはこの2つだけではありません。圧縮のあとには、sourceがcompactのSessionStartも発火します。SessionStartのmatcherにcompactを書けば、圧縮のたびに走る再注入用のフックになります。PostCompactとは入力も役割も違うので、3つは重なりません。

フック発火タイミング決定制御主な用途
PreCompact発火タイミング圧縮の直前決定制御あり(decision: "block"またはexit 2で圧縮を阻止)主な用途特定条件下で圧縮を止める
PostCompact発火タイミング圧縮の直後決定制御なし主な用途要約テキストの記録・外部状態の更新
SessionStart(compact)発火タイミング圧縮完了後のセッション再開処理として決定制御なし(additionalContext等の追加のみ)主な用途圧縮後のコンテキスト再注入

PostCompactが受け取れるのは要約テキストそのものです。SessionStartの入力に要約は含まれず、sourceなどのフィールドだけが渡されます。要約の中身を使った処理を書くならPostCompact一択です。

よくあるつまずき

matcherにツール名を書いてしまう

PreCompact・PostCompactのmatcherが絞り込むのは圧縮の引き金(manualまたはauto)で、PreToolUseのようなツール名ではありません。Bashのような値を書いても一致するイベントが無いため、フックが一度も起動しません。

PostCompactでブロックを試みる

PostCompactに決定制御は無く、exit 2もJSON出力のdecisionも圧縮結果を変更しません。圧縮を止める判断はPreCompact側でしか行えないので、圧縮後に条件を確認して差し戻したいという設計は成立しません。

systemMessageが表示されると思い込む

他の多くのイベントで使えるsystemMessageは、PreCompactとPostCompactのどちらでもClaude Codeが破棄します。ユーザーへの通知が必要なら、PreCompactのブロック時はstderr経由(手動/compactのみ画面に表示)、それ以外は外部ログやデスクトップ通知など別の経路を用意します。

自動圧縮のブロックを常時オンにする

マーカーファイル方式は、解除の手順まで決めてから導入します。.claude/no-compactを作った本人が忘れても、スクリプトは何も警告しません。自動圧縮のブロック理由は画面に出ないため、解除し忘れに自分で気づく手段がありません。ガードを入れた日付をマーカーファイルの中身に書いておくと、catで放置期間を確かめられます。

ifフィールドで絞り込もうとする

PreToolUseのハンドラでおなじみのifフィールドは、PreToolUse・PostToolUse・PostToolUseFailure・PermissionRequest・PermissionDeniedというツール絡みの5イベント専用です。PreCompactやPostCompactのハンドラにifを書いても評価対象にならず、そのハンドラ自体が一度も発火しません。絞り込みはmatcher(manual/auto)だけで行い、それ以上の条件分岐はスクリプト内部でtriggerやcwdを見て実装します。

asyncを付けて判断が間に合わなくなる

コマンドフックにはasync: trueで非同期実行にするオプションがありますが、PreCompactをブロック用途で使うならこれを付けてはいけません。非同期実行はClaude Codeの処理をブロックせずバックグラウンドで走るため、圧縮の可否を確定させる前に圧縮そのものが進んでしまいます。判断が必要なフックはasyncを付けない同期実行のままにします。

タイムアウトしても安全側に倒れない

command・http・mcp_tool型のフックがタイムアウトでキャンセルされると、多くのイベントと同様にPreCompactも出力が破棄され「決定なし」として扱われます。つまりガードスクリプトが固まって時間切れになっても圧縮はブロックされず、そのまま進行します。確実に止めたい処理を書くときは、フック自身の処理を軽くしてタイムアウトの可能性そのものを減らします。

worktree内でcwdとマーカーファイルの場所がずれる

Claude Codeがworktreeに入ってセッションを進めている場合、フックが受け取るcwdはworktree側のパスに変わります。一方${CLAUDE_PROJECT_DIR}はセッションを始めた元のプロジェクトルートを指したままです。

上のガードスクリプトのようにcwd基準でマーカーファイルを探すと、元のプロジェクトに置いたマーカーを見つけられません。${CLAUDE_PROJECT_DIR}は環境変数としてもスクリプトに渡されるので、マーカーの基準をこちらに寄せれば、worktreeをまたいでも同じ場所を見ます。

root="${CLAUDE_PROJECT_DIR:-$(jq -r '.cwd' <<<"$input")}"
[[ "$trigger" == "auto" && -f "$root/.claude/no-compact" ]]

設定する前に、スクリプト単体で動かして確かめる

hookはstdinのJSONを読んで終了コードで答えるだけなので、Claude Codeを起動しなくても手元で確かめられます。自動圧縮は数十万トークンの会話が溜まるまで起きません。事前にここで分岐を通しておくと、本番で初めて動くのが自動圧縮、という状況を避けられます。

手順

スクリプトを単体で試す流れ

  1. 1

    一時ディレクトリに2本のスクリプトを置く

    .claude/hooks/にguard-compact.shとlog-compact.shを作り、chmod +xで実行権限を付けます。

  2. 2

    マーカー無しでautoのJSONを流す

    終了コードが0で、何も表示されなければ通過です。

  3. 3

    マーカーを作り、autoとmanualを流し分ける

    autoだけが終了コード2になり、stderrに文言が出れば期待どおりです。

  4. 4

    PostCompactのJSONを流してログを見る

    compact-log.jsonlに1行増えます。

次の出力は、Claude Code v2.1.287が入った環境で上の手順を実行した結果です(jq 1.7.1)。

$ echo '{"cwd":"'$PWD'","trigger":"auto"}' | .claude/hooks/guard-compact.sh; echo "exit=$?"
exit=0
$ touch .claude/no-compact
$ echo '{"cwd":"'$PWD'","trigger":"auto"}' | .claude/hooks/guard-compact.sh; echo "exit=$?"
自動圧縮を一時停止中です(.claude/no-compactが存在)
exit=2
$ echo '{"cwd":"'$PWD'","trigger":"manual"}' | .claude/hooks/guard-compact.sh; echo "exit=$?"
exit=0
$ echo '{"cwd":"'$PWD'","trigger":"auto","compact_summary":"日本語の要約です"}' | .claude/hooks/log-compact.sh
$ cat .claude/compact-log.jsonl
{"ts":1791004957.104134,"trigger":"auto","summary_length":8}

summary_lengthは文字数で、バイト数ではありません。日本語8文字の要約が8になっています。tsはUNIX時刻の小数なので、人が読むログにしたいときはnow | todateに変えます。

まとめ

圧縮を止めたいならPreCompact、圧縮後の要約を外へ残したいならPostCompact、圧縮後にコンテキストを再注入したいならSessionStartのcompactを選びます。自動圧縮を止める場合は、上限エラー回復の経路で会話が失敗しうる点だけは先に織り込んでおくと安全です。ユースケース別の実装はHooks実例カタログにあります。

この記事を共有:XはてブLinkedIn