PreCompact/PostCompact hookでコンテキスト圧縮の前後に介入する
PreCompactは圧縮前に発火して自動圧縮を止められ、PostCompactは圧縮後の要約を受け取ります。設定例と、自動圧縮をブロックしたときに起こる分岐を解説します。
PreCompact/PostCompact hookとは
PreCompactはClaude Codeがコンテキストの圧縮(compaction)を実行する直前に、PostCompactは圧縮が完了した直後に発火するhookイベントです。圧縮は会話履歴を要約してコンテキストウィンドウの空きを作る処理で、手動の/compactと、コンテキストウィンドウが埋まったときに自動で走るauto-compactのどちらでも同じ2つのイベントが発火します。PreCompactは圧縮そのものを止められる数少ないイベントの一つで、PostCompactは圧縮結果を変えられない代わりに要約テキストを受け取れます。フックイベント全体の一覧はHooks完全ガイドにまとめてあるので、本記事はこの2つのイベントが受け取る情報と実装の分岐に絞ります。
前提条件
- Claude Codeの
hooks設定(.claude/settings.jsonなど)を編集できること jqコマンドが実行環境にインストールされていること(JSON入力を読むため)- compactionが何を残し何を捨てるかの前提はClaude Code compactの発火条件と要約後に残る情報、auto-compactが発火するしきい値の調整はClaude Code autocompactの使い方とコンテキスト圧縮の調整で扱っています。本記事はしきい値の調整ではなく、圧縮イベントそのものへのフック介入に絞ります。
手順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": []
}
]
}
]
}
}PreCompactが返すsystemMessageとcontinueフィールドはClaude Codeが破棄します。ブロック理由をユーザーに見せたい場合、手動の/compactならstderrのメッセージが画面に表示されますが、自動実行にはそもそもユーザー操作の直後という表示先がありません。
ハンドラは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つだけではありません。圧縮が完了した後、Claude CodeはsourceがcompactのSessionStartも発火させ、MCPサーバーの再接続のような通常のセッション開始処理をやり直します。要約によって会話の前提が変わった直後にMCPサーバーとの接続状態を作り直す目的で、PostCompactとは別枠の処理として走ります。3つの役割は重なりません。
| フック | 発火タイミング | 決定制御 | 主な用途 |
|---|---|---|---|
PreCompact | 発火タイミング圧縮の直前 | 決定制御あり(decision: "block"またはexit 2で圧縮を阻止) | 主な用途特定条件下で圧縮を止める |
PostCompact | 発火タイミング圧縮の直後 | 決定制御なし | 主な用途要約テキストの記録・外部状態の更新 |
SessionStart(compact) | 発火タイミング圧縮完了後のセッション再開処理として | 決定制御なし(additionalContext等の追加のみ) | 主な用途MCP再接続後のコンテキスト再注入 |
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のみ画面に表示)、それ以外は外部ログやデスクトップ通知など別の経路を用意します。
自動圧縮のブロックを常時オンにする
前述のとおり、コンテキスト上限エラーからの回復として走った自動圧縮をブロックすると、エラーがそのまま表面化してリクエストが失敗します。マーカーファイルのような一時停止の仕組みを使う場合は、長時間放置せず解除を忘れない運用ルールとセットで導入します。
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基準でマーカーファイルを探す実装は、worktree内では元のプロジェクトに置いたマーカーを見つけられません。worktreeをまたいで一貫させたい場合は、${CLAUDE_PROJECT_DIR}をargsの要素として渡し、スクリプト側は$1として受け取ってそちらを基準にパスを組み立てます(${CLAUDE_PROJECT_DIR}はJSON内の文字列置換なので、スクリプト内で環境変数として読むことはできません)。
まとめ
数多いhookイベントのうち、圧縮の前後という一点に絞って発火するのはPreCompactとPostCompactだけです。PreCompactは圧縮を止められる数少ないフックですが、ブロックの効果は発火理由(先回りかエラー回復か)で分かれるため、無条件の停止は避けるべきです。PostCompactは決定制御を持たない代わりに要約テキストそのものを受け取れるので、監査ログや外部状態の更新に向きます。圧縮後のSessionStart(source: "compact")とあわせて3つのタイミングを整理しておけば、どのフックに何を書くべきかで迷わなくなります。ユースケース別の実装はHooks実例カタログもあわせて参照してください。