リリース作業の途中で、作っていたプラグインに67%警告を鳴らされた: compact-ops を公開しました
7月7日の未明、私は compact-ops というClaude Codeプラグインのv0.2.0リリース作業をしていました。コンテキスト使用率が67%に達した瞬間、セッションに通知が注入されます。「使用率が閾値を超えました。切りのいいところで /compact を打ってください。現在のPlanはこれ、直近の判断はこれ」。この警告を書いたのは私で、守られたのも私でした。
面白かったのはその後です。実際に /compact を打ったら、今度は同じプラグインのhookが3本とも失敗しました。リリース作業でプラグインのディレクトリ構成を変えたため、走行中セッションが持っていた古いパス参照が切れていたのです。それでもcompaction自体は普通に完走しました。全hookをfail-open(失敗しても本体処理を止めない)で設計していたからです。1つのセッションの中で、機能が効く瞬間と、壊れても邪魔をしない瞬間の両方を踏みました。ドッグフーディングとしてはこれ以上ない密度です。
この記事では、/compact で何が消えるのか、先行実装である u-ichi さんの compact-plus から何を引き継ぎ何を変えたのか、public化の前に締めた6箇所を書きます。
/compact で消えるのは「コード」ではなく「運用系の事実」
Claude Codeはコンテキストが埋まると、会話全体をbuilt-inプロンプトで1本のsummaryに圧縮し、古いメッセージを捨てます。手動の /compact でも自動発火でも仕組みは同じです。
このsummaryは要点をよく拾います。消えやすいのは、コードの内容よりも運用系の事実のほうです。「pushはもう承認済み」「このアプローチは試して失敗した」「この値の出典はあのファイル」。圧縮後のagentがこれらを忘れると、承認を取り直しに来たり、失敗済みの手を打ち直したりします。標準動作とcompact-opsありの違いを時系列で並べるとこうなります。
| タイミング | 標準のClaude Code | compact-opsあり |
|---|---|---|
| 使用率60%超 | 無警告。auto-compactは突然来る | 1回だけ通知 + Plan/Phase/直近判断の3行を注入 |
| compactの瞬間 | built-inのsummary生成のみ | 同じ圧縮に加えて、transcript全文backup + 別LLMが10見出しのstate fileを書く |
| 圧縮直後 | summaryだけを頼りに再開 | state file + 「原文の再読を優先」noteを新コンテキストに注入 |
| セッションを閉じた後 | summaryはそのセッション内で消滅 | stateは30日残り、claude --resume 時も注入される |
| hookが失敗した時 | — | fail-open。標準のcompactはそのまま通る |
圧縮アルゴリズム自体には一切手を入れていません。公式hookだけで外側から保険をかける構成です。
compact-plus という先行実装
この設計の骨格は私のオリジナルではありません。u-ichiさんの compact-plus(MIT)が先に「PreCompact hookで別のLLMを呼び、構造化stateを書き出す」というアイデアを実装していました。読んだ瞬間に自分の環境へ入れたくなる種類のツールです。
ただ、私の運用にそのまま載せるには3つの前提差がありました。そこでforkではなく派生実装にして、次の3点を変えています。
- 使用率警告をプラグイン単体で完結させた。 compact-plusの警告は、作者の別リポジトリにあるstatuslineスクリプトが書くmarkerに依存していて、プラグインだけ入れても発火しません。compact-opsはUserPromptSubmit hookがtranscript末尾のusageから使用率を自己計算します。
- stateの置き場所を
$TMPDIRから~/.claude/compact-ops/に変えた。$TMPDIRのstateは再起動を挟むと消えます。翌日に続きをやる運用では、それでは保険になりません。プロジェクトごとに整理して30日保持にしました。 --resume時の復旧を足した。 compactを跨ぐ復旧だけでなく、翌日claude --resumeした時にもSessionStart hookが同じstateを注入します。再起動を挟んでも前日の判断が残ります。
LLM backendもClaude単体(Sonnet primary、Haiku fallback)に寄せました。compact-plusはfallbackにCodexを使う構成で、ChatGPT Pro前提になるためです。
state file は10見出しの固定フォーマット
圧縮前に別LLMが書き出すstate fileは、# Compact Prep State から始まる10見出しの固定構成です。Active Plan / Current Phase / TaskList Summary / Session Decisions / Constraints and Blockers / Worker Topology / Skills Invoked / Editing Files / Failed Attempts / Recovery Notes。summaryで薄まりがちな「判断と理由」「失敗済みアプローチ」に独立した見出しを割いているのがポイントです。
圧縮後のagentは、標準summaryとこのstate fileの二重の手がかりで再開します。ただしstate fileを正典にはしません。注入する復旧ガイダンスには「原文のプロジェクトファイルを再読してから信じろ」と明記しています。LLMが書いた要約を別のLLMが無条件に信じる構図は、伝言ゲームの高速化でしかないからです。
public 化の前に締めた6箇所
v0.1.0は動くものを最短で作った状態でした。公開するにあたって、自分のレビューとcodex CLIの二段レビューを掛けて、v0.2.0で6箇所を締めています。
- パーミッション。 state fileとtranscript backupには会話内容がそのまま残ります。ツール出力経由でsecretsが写り込む可能性もあるので、全hookに
umask 077を入れ、ディレクトリ700・ファイル600で作成します。 - session_idの検証。 hookの入力JSONから来る
session_idをそのままファイルパスに使っていました。allowlist検証(英数と._-のみ、..拒否)を通してから使います。 - LLM出力の検証。 state生成LLMが変な出力を返した場合、以前は1行目しか見ていませんでした。10見出し全部の存在を検証し、不正なら旧stateを保持したままfail-openします。
- jqの単一パス化。 transcriptの間引き処理が1行ごとにjqを3〜4回spawnしていて、長いセッションではhookのtimeout予算を食い潰すリスクがありました。1プロセスのストリーム処理に書き直しています。
- backupのgzip化。 transcriptのJSONLは圧縮で約1/10になります。
- デバッグログ。 これが一番の教訓です。fail-open設計は本番で邪魔をしない代わりに、静かに死にます。冒頭の「hook3本失敗」も、エラーメッセージが出たから気づけただけで、原因調査の足場は最初ありませんでした。
COMPACT_OPS_DEBUG=1で、握りつぶした失敗理由をログに残せるようにしています。fail-openを書くなら、その前にログを書くべきでした。順序を間違えたという話です。
インストール
git clone https://github.com/kenimo49/compact-ops.git
claude plugin marketplace add /path/to/compact-ops --scope user
claude plugin install compact-ops@compact-ops-local
前提はClaude Code v2.x、jq、backend用の claude CLIです。入れたあとは普通に /compact を打つだけで動きます。仕組みの詳細はリポジトリのREADMEに日英で書きました。
このプラグインの最初の受益者は、これを書いていたセッション自身でした。67%の警告に救われ、hookの失敗に設計思想を試された1日ぶんのログが、そのままリリースノートの裏付けになっています。並列セッションのハーネス制約の話と同じで、agentの運用は事故が起きた場所にしか実装の理由が書けません。あなたの環境で /compact 後のagentが何を忘れるか、一度観察してみてください。
この記事は役に立ちましたか?