長時間セッションでClaude Codeの会話圧縮の前後にログ・通知・退避処理を仕込みたい中級開発者向け
長時間のClaude Codeセッションで会話履歴の自動圧縮が走る前後に、重要メモの退避・圧縮ブロック・圧縮後ログ記録などの自動処理を仕込みたい場面で、settings.jsonのhooksブロックにPreCompactかPostCompactの設定を書いて使う
長時間 Claude Code を動かしていると、会話のやり取りが膨らみすぎてトークンを消費し尽くす場面が出てきます。そこで走るのが context compaction。過去のやり取りを要約して圧縮する処理です。PreCompact と PostCompact は、その圧縮処理の直前と直後に発火するフックの双子セット。
用途はくっきり分かれます。PreCompact は「圧縮される前にメモを退避させる」「危ない場面なら圧縮そのものを止める」役。PostCompact は「圧縮が終わったあとに、どこまで残ったかを記録する」観察役。片方を入れたら片方も入れる、というよりは役割を理解して使い分けるのが筋です。
噛み砕くと、家の引っ越しに似ている
引っ越し業者がトラックに荷物を積む直前、最後にもう一度部屋を見回して「これは置いていけない、別の段ボールに入れて持つ」とやる瞬間が PreCompact。トラックが出発した後、空になった部屋を確認して「冷蔵庫の中身全部捨てちゃった、メモしておこう」とやるのが PostCompact。
圧縮は要約なので、細かいニュアンスや「途中で決めた小さな約束」は普通に消えます。消えてほしくない情報があるなら、消える前に別の場所に逃がしておく。これが PreCompact の存在意義です。
大事な前提:matcher の値はツール名ではなく「圧縮の起源」
PreToolUse や PostToolUse のような他のフックに慣れていると、matcher に Bash や Edit のようなツール名を書く感覚が染み付いています。PreCompact / PostCompact ではここが違う。
matcher に書けるのは manual か auto の2択。前者は私が /compact を手で叩いた時、後者は会話履歴が満杯になって Claude Code 側が自動で圧縮を走らせた時です。Bash と書いても永遠に発火しません。
「cooking-blog」を例に、実際の手順を見る
長期メンテしている料理ブログのプロジェクト cooking-blog で、何時間も Claude Code と作業した状況を想定します。記事構成・撮影プラン・公開予定日の3つは、圧縮で要約されると確実にニュアンスが落ちる情報。これを PreCompact で退避し、PostCompact で圧縮後の状態を記録する流れを組みます。
ステップ1: settings.json に hooks ブロックを書く
プロジェクト直下の .claude/settings.json を開いて、hooks の項目に PreCompact と PostCompact を併記します。
{
"hooks": {
"PreCompact": [
{
"matcher": "manual",
"hooks": [
{
"type": "command",
"command": "~/cooking-blog/.claude/scripts/save-memo.sh"
}
]
},
{
"matcher": "auto",
"hooks": [
{
"type": "command",
"command": "~/cooking-blog/.claude/scripts/save-memo.sh"
}
]
}
],
"PostCompact": [
{
"matcher": "manual",
"hooks": [
{
"type": "command",
"command": "~/cooking-blog/.claude/scripts/log-compact.sh"
}
]
},
{
"matcher": "auto",
"hooks": [
{
"type": "command",
"command": "~/cooking-blog/.claude/scripts/log-compact.sh"
}
]
}
]
}
}
manual と auto を両方並べているのは、私が /compact を叩いた時も、自動圧縮が走った時も、同じスクリプトを走らせたいからです。片方しか書かないと、もう片方の起源では発火しません。
ステップ2: PreCompact 用スクリプトで「圧縮前の transcript」を退避
PreCompact のスクリプトには「圧縮前のtranscript(会話履歴ファイル)を別名でコピーする」処理を書きます。フックには JSON で session_id や transcript_path が標準入力で渡ってくるので、それを読んで使う形。
#!/bin/bash
# save-memo.sh
INPUT=$(cat)
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path')
SESSION=$(echo "$INPUT" | jq -r '.session_id')
BACKUP_DIR=~/cooking-blog/.claude/transcript-backup
mkdir -p "$BACKUP_DIR"
cp "$TRANSCRIPT" "$BACKUP_DIR/before-compact-$SESSION-$(date +%Y%m%d-%H%M%S).jsonl"
exit 0
これで圧縮直前の素のtranscriptが、別ファイルとしてプロジェクト内に残ります。あとから「何を覚えていたか」を完全な形で読み返せる。
ステップ3: ここで初心者がやりがちな勘違い
PreCompact 内で transcript_path を「あとで読もう」と思って path だけメモして帰る、をやると失敗します。圧縮後に同じ path を読み直すと、中身は圧縮後の transcript に上書きされているからです。
素の中身を残したいなら、PreCompact のスクリプト内で cp なり別の保存場所への書き出しなりをその場で完了させる。これは「path を持って帰る」感覚で組むと確実に踏みます。
ステップ4: PostCompact 用スクリプトで「圧縮後の状態」を記録
PostCompact 側は、圧縮済みの transcript を読んで「行数・サイズ・最後のメッセージのタイムスタンプ」を簡易ログに追記する役。圧縮で何がどれだけ削られたかを定点観測する用途です。
#!/bin/bash
# log-compact.sh
INPUT=$(cat)
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path')
SESSION=$(echo "$INPUT" | jq -r '.session_id')
LINES=$(wc -l < "$TRANSCRIPT")
SIZE=$(wc -c < "$TRANSCRIPT")
LOG=~/cooking-blog/.claude/compact-log.txt
echo "$(date '+%Y-%m-%d %H:%M:%S') session=$SESSION lines=$LINES bytes=$SIZE" >> "$LOG"
exit 0
ステップ5: 圧縮を止めたい場面の組み方
PreCompact は exit 2 または stdout に {"decision": "block", "reason": "..."} を返すと、圧縮そのものを止められます。たとえば「今ちょうど重要な記事構成を詰めている、勝手に要約されたら困る」というガードを入れたいなら、スクリプト側で「直近10分以内に重要メモが更新されているか」を見て、該当すれば block を返す形。
#!/bin/bash
# block-if-busy.sh
RECENT=$(find ~/cooking-blog/notes -newermt '10 minutes ago' | wc -l)
if [ "$RECENT" -gt 0 ]; then
echo '{"decision":"block","reason":"重要メモ更新中。/compact は10分待ってから"}'
exit 0
fi
exit 0
ステップ6: PostCompact では止められない
同じノリで PostCompact に block を書いても、圧縮はすでに完了しているので戻せません。PostCompact の exit 2 は標準エラー出力(stderr)を私の画面に表示するだけ。「圧縮された後で気づいたから巻き戻して」は仕様上不可能です。
つまり PreCompact / PostCompact は何をしてくれるのか
- やってくれる(PreCompact): 圧縮直前に任意のスクリプトを走らせる、圧縮を止める、transcript の素の中身を退避する
- やってくれる(PostCompact): 圧縮直後に観察用スクリプトを走らせる、ログ・通知・後片付けを自動化する
- やってくれない: PostCompact から圧縮の巻き戻し、特定メッセージだけを圧縮対象から除外する細かい指定、圧縮アルゴリズムの差し替え
- 意味が薄い場面: 1セッション30分以内で終わる短い作業(そもそも圧縮が走らない)、フック用スクリプトを書くより手で
/compact前に都度メモを取る方が早い個人作業
使いどころ3シナリオ(具体題材で再現)
シナリオ1: 料理ブログの長丁場リライト中
料理ブログ cooking-blog で、過去50記事を順に開いて Claude にリライト指示を出していく作業。3時間も続けると会話履歴が膨らんで自動圧縮が走り、「8記事目で決めたリライト方針」が要約で消えがちです。PreCompact で ~/cooking-blog/.claude/transcript-backup/ に素の transcript を退避しておけば、消えた後に「何を決めていたか」を完全な形で参照し直せる。matcher は auto を狙う。
シナリオ2: 家計簿アプリ開発の最中、勝手な圧縮を止める
家計簿アプリ kakeibo-app の認証まわりを Claude と詰めていて、いま画面遷移の細かい仕様を口頭ベースで詰めている真っ最中。ここで自動圧縮が走ると、まだ SPEC.md に落とせていない口頭仕様が要約で粗くなります。PreCompact の matcher auto 側で「SPEC.md が直近10分以内に更新されていなければ圧縮を block する」スクリプトを置く。手動圧縮、つまり matcher: "manual" 側は私の意思なので素通しにする。
シナリオ3: OSS clone 直後、圧縮ログを定点観測
適当な OSS を git clone してきて Claude に構造を読ませる流れ。何時間どのくらい会話してから自動圧縮が走るのか、最初は感覚がつかめません。PostCompact で「圧縮が走った時刻・圧縮後の transcript の行数・session_id」を ~/work/compact-log.txt に追記しておくと、自分の使い方だと何分くらいで限界が来るかが数字で見える。matcher は manual も auto も両方仕掛けて差を測る。
初心者が踏みやすい落とし穴
- matcher にツール名を書いて発火させようとする。PreCompact / PostCompact の matcher は
manualとautoの2択のみ。BashEditと書いても永遠に動かない - PreCompact で path だけメモして圧縮後に読みに行く。同じ
transcript_pathは圧縮後に中身が上書きされる。PreCompact のスクリプト内でcpなりしてその場で退避する - PostCompact で圧縮を止めようとする。PostCompact は事後通知。
exit 2も stdout のdecision: "block"も、圧縮を巻き戻す効果はない。stderr が画面に出るだけ - matcher を片方しか書かず、もう片方で発火しない。
manualだけ書いて自動圧縮で走らないと「フックが動いてない」と勘違いする。両方仕掛ける - session_id を全フックで共通の鍵だと思い込む。session_id は1会話セッション単位なので、Claude Code を再起動すれば変わる。長期ログでセッションをまたいで突き合わせるなら別の識別子と組み合わせる
- スクリプトが標準入力からの JSON を読まずに動かす。フックには
session_idtranscript_pathcwdhook_event_nameが標準入力で渡ってくる。catで受けてjqで抜くのが定番 - compact-log.txt がプロジェクト直下の Git 管理下にある。圧縮ログには場合によってはセンシティブな情報が含まれる可能性がある。
.gitignoreに必ず追加する
書き方
settings.jsonに以下を書く(matcherは manual か auto の2択、複数並べて両方の起源で発火させる)
{
"hooks": {
"PreCompact": [
{
"matcher": "manual",
"hooks": [
{ "type": "command", "command": "~/project/.claude/scripts/save-memo.sh" }
]
}
],
"PostCompact": [
{
"matcher": "auto",
"hooks": [
{ "type": "command", "command": "~/project/.claude/scripts/log-compact.sh" }
]
}
]
}
}
やってみるとこうなる
入力
/compact を手で叩く(matcher: "manual" 側が発火)
または会話履歴が満杯になって自動圧縮が走る(matcher: "auto" 側が発火)
フックスクリプトには標準入力で次のようなJSONが渡る:
{
"session_id": "abc123",
"transcript_path": "~/.claude/projects/cooking-blog/transcript.jsonl",
"cwd": "~/cooking-blog",
"hook_event_name": "PreCompact"
}
出力例
PreCompact側のsave-memo.shが走り、transcript(会話履歴ファイル)が ~/cooking-blog/.claude/transcript-backup/before-compact-abc123-20260519-031200.jsonl に退避される。圧縮が完了したあとPostCompact側のlog-compact.shが走り、~/cooking-blog/.claude/compact-log.txt に「2026-05-19 03:12:05 session=abc123 lines=82 bytes=41200」のような1行が追記される。
PreCompactで圧縮を止めたい場合は標準出力に {"decision":"block","reason":"..."} を返すかexit code 2(スクリプトが終了時に返す数字の2)を返す。PostCompactは何を返しても圧縮を巻き戻せず、exit 2はstderr(エラーメッセージの出力先)が画面に出るだけ
このページに出てきた言葉
- context compaction
- Claude Codeが会話履歴を要約して圧縮する機能。トークン消費が限界に近づくと走り、過去のやり取りを短くまとめ直してメモリを空ける
- トークン
- AIが文章を扱う最小単位。日本語1文字でだいたい1〜2トークン消費し、会話が長引くと積み上がって上限に達する
- フック
- Claude Codeの特定のイベント(ファイル編集前、コマンド実行後、会話圧縮の直前など)に合わせて、自前のスクリプトを差し込める仕組み
- matcher
- フック設定で「どの条件のときに発火させるか」を指定する項目。PreCompact / PostCompactでは <code>manual</code>(/compactを手で叩いた)か <code>auto</code>(自動圧縮)の2択
- transcript
- 会話履歴を1行1メッセージで記録したファイル。Claude Codeが内部的に <code>.jsonl</code> 形式で保存し、フックには <code>transcript_path</code> でその場所が渡される
- stdout
- 標準出力。スクリプトが <code>echo</code> で吐いた文字列の行き先。フックでは特定形式のJSONをここに出して挙動を制御する
- exit code
- スクリプトが終了時に返す数字。<code>0</code>が正常終了、<code>2</code>はフック規約で「block」を意味する特殊コード
- settings.json
- Claude Codeの設定ファイル。プロジェクト直下の <code>.claude/settings.json</code> または <code>~/.claude/settings.json</code> に置く