SessionEnd hookでセッション終了時に後始末を自動化する
SessionEnd hookはセッションが終わる直前に呼ばれ、終了理由別のmatcherで後始末を分けられます。決定不可・既定1.5秒というタイムアウト制約の中での書き方を示します。
SessionEnd hookはいつ、何のために呼ばれるか
SessionEnd hookは、Claude Codeのセッションが終了するタイミングで呼ばれるフックです。用途はクリーンアップ処理、セッション統計のログ記録、セッション状態の保存の3つです。セッションが終わる合図そのものだと考えると使い道が見えやすくなります。
呼ばれるのはセッションの終了時だけではありません。/clearによるクリア、インタラクティブな/resumeでのセッション切り替えでも発火します。どの操作で終了したかは入力JSONのreasonフィールドに入り、値は次の5つです。
| reason | 意味 |
|---|---|
clear | 意味/clearコマンドでセッションをクリアした |
resume | 意味インタラクティブな/resumeで別セッションへ切り替えた |
logout | 意味ユーザーがログアウトした |
prompt_input_exit | 意味プロンプト入力欄が表示された状態で終了した |
other | 意味上記に当てはまらないその他の終了 |
bypass_permissions_disabledという値も表には出てきますが、v2.1.234で廃止されており、Claude Codeはもう送ってきません。古い設定を流用したmatcherに残っていたら削除します。
入力JSONはsession_id・transcript_path・cwd・hook_event_nameといった共通フィールドにreasonが加わった形です。Claude Code v2.1.257以降であればscratchpad_dir(セッションの作業用一時ディレクトリ)も届くので、セッション中に貯まった一時ファイルをここから辿って削除する後始末も書けます。
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"hook_event_name": "SessionEnd",
"reason": "other"
}終了理由ごとにmatcherで振り分ける
SessionEnd hookのmatcherは、このreasonフィールドの値でフィルタします。matcherの評価規則は他のフックと共通です。英数字・アンダースコア・ハイフン・スペース・カンマ・パイプだけで書いたmatcherは完全一致(または|・,区切りの複数完全一致)として扱われ、それ以外の文字を含むとJavaScriptの正規表現として評価されます。
reasonの値はアルファベットとアンダースコアだけなので、clearやresume|logoutのように書けばそのまま完全一致で動きます。カンマ区切り(resume, logoutのような空白付きの書き方を含む)を使うにはClaude Code v2.1.191以降が必要です。
matcherを空にする、または省略すると全ての終了理由で発火します。理由ごとに処理を分けたいときだけ、次のように複数のmatcherグループを並べます。
{
"hooks": {
"SessionEnd": [
{
"matcher": "clear|resume",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-cleanup.sh"
}
]
},
{
"matcher": "logout|other",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-log.sh",
"timeout": 5
}
]
}
]
}
}clearとresumeは同じセッション内で作業が続く終了、logoutとotherはセッションそのものが閉じる終了です。この境目でスクリプトを分けると、後始末の重さを揃えやすくなります。
設定例 — ログ書き出しとクリーンアップスクリプト
設定は.claude/settings.json(プロジェクト共有)・~/.claude/settings.json(自分の全プロジェクト)・.claude/settings.local.json(gitignore対象)のいずれかに書きます。スキルやサブエージェントのfrontmatterに直接書くこともできます。
上の設定例が呼ぶsession-log.shの中身です。標準入力で受け取ったJSONからreason・session_id・cwdを取り出し、1行のログに追記します。
#!/bin/bash
# .claude/hooks/session-log.sh
input=$(cat)
reason=$(jq -r '.reason' <<<"$input")
session_id=$(jq -r '.session_id' <<<"$input")
cwd=$(jq -r '.cwd' <<<"$input")
echo "$(date -Iseconds) session=$session_id reason=$reason cwd=$cwd" >> ~/.claude/session-history.logコマンドは${CLAUDE_PROJECT_DIR}で参照しています。作業ディレクトリが途中でworktreeに変わっても、このプレースホルダーはセッションが始まったプロジェクトルートを指し続けるので、hookスクリプトの置き場所が動きません。現在の作業ディレクトリを知りたいときは、パスではなく入力JSONのcwdフィールドを読みます。
どの設定ファイルに書くか — スコープの選び方
同じSessionEnd hookでも、どのファイルに書くかで効く範囲が変わります。
| 定義場所 | 有効範囲 | 共有 |
|---|---|---|
~/.claude/settings.json | 有効範囲自分の全プロジェクト | 共有不可(自分のマシンのみ) |
.claude/settings.json | 有効範囲単一プロジェクト | 共有可(リポジトリにコミット) |
.claude/settings.local.json | 有効範囲単一プロジェクト | 共有不可(gitignore対象) |
| 組織のmanaged policy settings | 有効範囲組織全体 | 共有可(管理者制御) |
プラグインのhooks/hooks.json | 有効範囲プラグイン有効時 | 共有可(プラグインに同梱) |
チームで統一したい後始末(ログアウト時のキャッシュ削除など)は.claude/settings.jsonに書いてコミットし、自分専用の統計収集は~/.claude/settings.jsonに置く、という使い分けが基本です。ここで1点注意があります。Claude Code on the webのクラウドセッションはローカルの~/.claude/settings.jsonを読みません。クラウド側でも同じ後始末を効かせたいなら、リポジトリにコミットする.claude/settings.json側に定義を寄せます。
設定は複数のファイルにまたがっていても上書きではなくマージされます。ユーザー・プロジェクト・ローカルの各設定は互いのSessionEnd hookを消さずに追加し合う仕組みなので、個人用とチーム共有用を安心して併用できます。
登録したSessionEnd hookがどのファイルから読み込まれているかは、Claude Code内で/hooksと入力すると確認できます。イベントごとの登録数とmatcher、handlerの中身を読み取り専用で見られます。一時的に止めたいときは設定ファイルからエントリを削除するか、"disableAllHooks": trueを設定します。ただしこれは全hookをまとめて止める設定で、SessionEnd hookだけを個別に無効化する仕組みはありません。
SessionEnd hookが抱える制約とよくあるつまずき
SessionEnd hookは他の多くのフックと違い、決定権を持ちません。セッションの終了そのものはブロックできず、できるのは後始末だけです。systemMessageを含むJSON出力フィールドは、返しても全て捨てられます。exit code 2を返しても止まりません。ユーザーにstderrが表示されるだけで、終了処理はそのまま進みます。
タイムアウトも独特です。SessionEnd hookには既定で1.5秒という共有の時間予算しかありません。これはセッション終了・/clear・インタラクティブな/resumeのいずれにも適用されます。個別のhookにtimeoutを長く設定すると、予算はその最大値まで自動的に引き上げられます(上限60秒)。ただしプラグイン提供のhookに設定したtimeoutは予算を引き上げません。予算そのものを明示的に上書きしたいときは、環境変数CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MSをミリ秒単位で指定します。
CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS=5000 claudeもう一つ見落としやすいのが、作業ディレクトリが消えているケースです。hookはClaude Codeの現在の作業ディレクトリで実行されますが、worktreeや一時ディレクトリが別プロセスによって削除済みだと、Claude Codeはセッション開始時のディレクトリ→プロジェクトルート→ホームディレクトリ→システムの一時ディレクトリの順にフォールバック先を探します。使ったフォールバック先はデバッグログに警告として残ります。
複数のSessionEnd hookが同じreasonに一致した場合、それらは並列に実行されます。同じhandlerをプロジェクト設定とローカル設定の両方に重複して書いても、実行は1回にまとめられるのでログが二重に記録される心配はありません。ただしプラグインやスキルが定義した同じ内容のhandlerは別扱いになり、両方が実行されます。
hookプロセスは呼び出し元の環境変数をそのまま引き継ぎますが、OTEL_*系のエクスポーター変数はClaude Codeが全てのサブプロセスから取り除きます。テレメトリの送信先URLなどを環境変数経由でSessionEnd hookに渡す設計にするときは、除去される変数に依存しないようにします。
使い分け早見表 — 終了理由別の後始末アイデア
| 終了理由 | 発生する操作 | 後始末の例 |
|---|---|---|
clear | 発生する操作/clearでセッションをクリア | 後始末の例一時キャッシュの削除、会話サマリーの保存 |
resume | 発生する操作/resumeで別セッションへ切り替え | 後始末の例セッション切り替えのログ記録 |
logout | 発生する操作ユーザーがログアウト | 後始末の例ローカルの認証キャッシュの削除 |
prompt_input_exit | 発生する操作プロンプト入力欄を開いたまま終了 | 後始末の例未送信の統計・ログのフラッシュ |
other | 発生する操作それ以外の終了(プロセス終了など) | 後始末の例セッション統計の確定保存 |
clearとresumeはセッション操作の一部として頻繁に発生します。重い処理を割り当てると1.5秒の予算をすぐ使い切るので、書き込みは軽くしておくのが無難です。logoutとotherは発生頻度が低い分、多少時間のかかる後始末を割り当てても影響が出にくくなります。
SessionEnd hookはStop hookやPreCompactと何が違うか
セッションのライフサイクルには終了に関わるフックが複数あります。役割の違いは決定権の有無で分かれます。
Stop hookは1つの会話ターンが終わる瞬間に呼ばれ、exit code 2でClaudeを止めずに継続させられます。処理を続けさせたいときの制御点です。エラーで異常終了した場合の扱いはStopFailure hookでAPIエラー終了時のフォールバックを書くで扱っています。PreCompactはコンパクション(会話履歴の圧縮)の直前に呼ばれ、同じくブロックできます。
SessionEndはこれらと違い、セッションという単位そのものの終わりに呼ばれ、ブロックする手段を持ちません。会話を続けさせるためのフックではなく、後片付けのためのフックだという位置付けです。セッションの立ち上がり側にあるSetupやInstructionsLoadedのようなイベントについてはClaude Code Setup/InstructionsLoadedフックの実務設定にまとめています。
hookをシェルコマンドではなくHTTPエンドポイントで受けたい場合、SessionEndを含む多くのイベントはtype: "http"でも定義できます。ただしSessionEndは出力を破棄するため、HTTP側で返す決定フィールドに意味はありません。副作用としてリクエストを送るだけの用途に限られます。HTTPで受ける設定の詳細はClaude CodeのHooksをHTTPエンドポイントで受けるで解説しています。
matcherの評価規則(完全一致か正規表現か)はイベントをまたいで共通の仕組みです。ツール呼び出し系のイベントでの具体的な書き方はPreToolUse hookでツール実行前に許可・拒否・改変するが参考になります。
まとめ
SessionEnd hookは、セッションが終わる瞬間を捉えて後始末を差し込むための仕組みです。終了理由別にmatcherを分けられる一方、決定権を持たず、既定のタイムアウト予算も1.5秒しかありません。セッション統計を集計したいチーム、ログアウト時にローカルキャッシュを消したいチーム、一時ファイルを溜め込みがちなワークフローを使っているチームであれば、設定を検討する価値があります。重い処理を割り当てるときは、timeoutかCLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MSで予算を明示的に引き上げておくと、途中で打ち切られる事故を避けられます。まずはログ1行を書き出すだけの軽い構成から始め、必要に応じてreasonごとの処理を足していく進め方が扱いやすいはずです。