Claude Code hooks でセッション状態を追跡する
Claude Code の hooks は「このツールをブロックする」「編集に lint をかける」といった ポリシー制御の文脈で語られがちですが、実はセッションが今何をしているか—— 処理中か、完了したか、質問でブロックしているか——を教えてくれる唯一の公開 API でもあります。 このガイドでは hook イベントから信頼できるセッション状態を導出する方法と、素朴な実装が 壊れるポイントを解説します。私たちのアプリ AgentManager も この土台の上に作られています。
hooks を 1 分で
hook は、
Claude Code がライフサイクルイベントのたびに実行するシェルコマンドです。stdin から
JSON ペイロード(session_id・hook_event_name・cwd と
イベント固有のフィールド)を受け取ります。登録先は ~/.claude/settings.json
(全プロジェクト共通)またはプロジェクトの .claude/settings.json:
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "$HOME/.claude/scripts/on-stop.sh" }
]
}
]
}
}
状態追跡に関係するイベントは次のとおりです:
| イベント | 発火タイミング |
|---|---|
SessionStart | セッションの開始・再開 |
UserPromptSubmit | プロンプトの送信 |
PreToolUse / PostToolUse | 各ツール呼び出しの前後 |
Notification | Claude が入力を必要とするとき(許可ダイアログ、放置リマインダー等) |
Stop | メインエージェントの応答完了 |
SubagentStop | サブエージェント(Task)の完了 |
SessionEnd | セッションの終了 |
イベントから状態へ
知りたいことは 4 状態に集約できます: 確認待ち(あなたでブロック中)、 処理中(作業中)、 完了(ターン終了)、 待機(開いているが何も起きていない)。まずは素朴な対応表から:
| イベント | 新しい状態 | 理由 |
|---|---|---|
SessionStart | 待機 | まだ何も頼まれていない |
UserPromptSubmit | 処理中 | 入力できた=ダイアログは残っていない。ターン開始 |
PreToolUse・PostToolUse | 処理中 | ツール活動=作業進行中 |
AskUserQuestion / ExitPlanMode の PreToolUse | 確認待ち | これらのツールは即座にあなたの回答(選択/プラン承認)でブロックする |
Notification(permission_prompt) | 確認待ち | 許可ダイアログが未応答のまま表示されている |
Notification(idle_prompt) | 完了 | ターン終了後の放置リマインダーであって新しい質問ではない |
Stop | 完了 | ターン完了 |
SessionEnd | (削除) | セッション消滅 |
動く最小実装
1 つのスクリプトをいくつかのイベントに登録し、session_id ごとに小さな
JSON ファイルを 1 つ書きます。あとはステータスバーウィジェットでも tmux のセグメントでも
ダッシュボードでも、そのディレクトリを描画すればいい。~/.claude/settings.json:
{
"hooks": {
"UserPromptSubmit": [
{ "hooks": [ { "type": "command", "command": "$HOME/.claude/track-status.sh running" } ] }
],
"Notification": [
{ "hooks": [ { "type": "command", "command": "$HOME/.claude/track-status.sh waiting" } ] }
],
"Stop": [
{ "hooks": [ { "type": "command", "command": "$HOME/.claude/track-status.sh done" } ] }
],
"SessionEnd": [
{ "hooks": [ { "type": "command", "command": "$HOME/.claude/track-status.sh ended" } ] }
]
}
}
~/.claude/track-status.sh はこう書きます(jq が必要。chmod +x を忘れずに):
#!/bin/bash
# stdin の hook ペイロードを読み、セッションごとの状態ファイルを保つ。
STATE="$1"
DIR="$HOME/.claude/session-status"
mkdir -p "$DIR"
PAYLOAD=$(cat)
SESSION_ID=$(echo "$PAYLOAD" | jq -r '.session_id')
CWD=$(echo "$PAYLOAD" | jq -r '.cwd')
if [ "$STATE" = "ended" ]; then
rm -f "$DIR/$SESSION_ID.json"
else
# アトミックに書く(一時ファイル + mv)。読み手に書きかけを見せない。
jq -n --arg state "$STATE" --arg cwd "$CWD" \
'{state: $state, cwd: $cwd, updated: now | todate}' \
> "$DIR/$SESSION_ID.json.tmp" && mv "$DIR/$SESSION_ID.json.tmp" "$DIR/$SESSION_ID.json"
fi
これで cat ~/.claude/session-status/*.json が全セッションのライブな状態を返します。
設計上の注意は 2 つ、上のコードに織り込み済みです: ファイルのキーは作業ディレクトリではなく
session_id(同じディレクトリで複数セッションは普通に起きる)、書き込みはアトミックに。
この 4 イベント版は対応表の細かい確認待ち(AskUserQuestion・ExitPlanMode・
idle_prompt)を省いているので、動いたら足してください。
素朴な実装が壊れるエッジケース
ここからが本題です。実際にデバッグ時間を溶かすのはこの 4 つで、先に知っておくことにこのガイドの価値の大半があります。
1. 許可ダイアログへの「応答」はイベントを発火しない
hook が教えてくれるのは許可ダイアログが出たこと(Notification)までで、
あなたが答えたことは分かりません。次のイベントが来るのは、承認したツールが完了したとき
(PostToolUse)かターンが終わったとき(Stop)。長いビルドを承認すると、
ビルド中ずっと「確認待ち」表示が残ります。これは設計で受け止めるしかありません:
確認待ちは「次のライフサイクルイベントで解除される」ものとして扱い、解除の遅れは許容する。
誤りは少なくとも安全側です(「確認待ちの残留」であって「確認待ちの見逃し」ではない)。
2. Stop は「全部止まった」を意味しない
バックグラウンドのサブエージェントは、メインエージェントの Stop 後も動き続けます。
サブエージェントの作業中に Stop で「完了」に倒すと、状態が嘘をつきます。
サブエージェントのライフサイクルを追跡し、「メインが停止済みかつサブエージェントが
残っていない」ときだけ完了に倒してください。さらにその記録は SessionStart で
リセットすること——resume でも発火するので、リセットしないと消えたサブエージェントの ID の
せいで永遠に「処理中」に固定されます。
3. 順序の罠: Stop に「確認待ち」を踏み潰させない
バックグラウンドのサブエージェントが許可待ちでブロック → Notification が
確認待ちを告げる → メインエージェントが終了 → Stop が完了を告げる。
純粋な「最後のイベント勝ち」では、本当にブロックしているセッションが隠れてしまいます。
対策は、確認待ちの理由(メイン由来かサブエージェント由来か)を追跡し、
対応する解除信号だけがフラグを降ろせるようにすること。放置中に定期的に再発火する
idle_prompt にも同じ配慮が必要です。
4. 異常終了はファイルを残す
SessionEnd は保証されません。クラッシュや強制 kill では発火せず、
サブエージェントを ESC で中断すると SubagentStop も発火しません。
長期運用するならガベージコレクションが要ります(例: 所有プロセスが消えた状態ファイルは破棄)。
自作するか、インストールするか
ここまでの内容は、1 つの用途・1 つの表示形式に絞れば半日で作れます。エッジケースの 対応は実際に踏むたびに固めていけばいい。それが楽しいなら良い半日です (私たちは明らかに楽しみました)。もう一つの選択肢:
AgentManager はこのパイプラインを macOS 向けに製品化したものです。 hook がセッションごとの状態ファイルを書き、常時最前面の小さなウィンドウが全セッションを 状態ランプ付きで描画します — 確認待ち (返答の形つき: 許可/プラン/選択)、 処理中、 完了、 待機。 上のエッジケースはすべて対応済みで、許可を承認した瞬間に「確認待ち」は即時に消え、残留しません。 さらに hook スクリプトには収まらない部分——確認待ち発生時のウィンドウ自動表示、 クリックで該当ターミナルペインへのジャンプ——も含みます。hooks の登録はワンクリックで、 きれいに解除でき、自作の hooks とも共存します。
セッション状態、作らずに使う
AgentManager は Claude Code の hook イベントを全セッションのライブな ステータスボードに変えます。無料で使えます — アカウント登録不要。