はじめに / 対象と前提
Claude Code を毎日使っていると、「昨日どこまでやったか」を思い出すのに時間を使っていないだろうか。セッションを開始するたびに STATE.md や直近のコミットログを手動で読ませているなら、それは SessionStart hook で自動化できる。
この記事は以下を前提にしている。
- Claude Code v2.0 系(hooks 機能が安定している最新版)
- Node.js 22.x(hook スクリプトを Node で書く場合)/ Bash でも可
-
.claude/settings.jsonでのプロジェクト単位フック設定に慣れている、または触ったことがある人
対象読者は「Claude Code の PreToolUse / PostToolUse hook は使ったことがあるが、SessionStart はまだ」という人。
TL;DR
-
SessionStarthook はmatcherにstartup/resume/clear/compactの 4 種類があり、それぞれ発火タイミングが違う - hook の stdout に JSON を返すと
additionalContextとしてモデルのコンテキストに直接差し込める(ファイルを読ませる指示を書くより確実) - 3 つのハマりどころ:①matcher 未指定で毎回全種類に発火する、②CLAUDE.md と内容が重複してトークンを無駄食いする、③hook のタイムアウトでセッション起動がブロックされる
手順 / 動かし方
1. hook スクリプトを用意する
前回の作業メモ(例:STATE.md の末尾数行)を JSON で返すだけの最小スクリプト。
#!/usr/bin/env bash
# .claude/hooks/session-start-context.sh
set -euo pipefail
STATE_FILE="./STATE.md"
if [ -f "$STATE_FILE" ]; then
SUMMARY=$(tail -n 20 "$STATE_FILE")
# JSON エスケープは jq に任せるのが安全(手書きだと改行・引用符で壊れる)
ESCAPED=$(printf '%s' "$SUMMARY" | jq -Rs .)
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": ${ESCAPED}
}
}
EOF
else
echo '{}'
fi
jq -Rs . で文字列全体を安全にエスケープしている点がポイント。手書きで \" 置換をやると改行や制御文字を含む Markdown で高確率で壊れる。
2. settings.json に登録する
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/session-start-context.sh",
"timeout": 5
}
]
}
]
}
}
matcher は正規表現扱いなので startup|resume のように OR 条件を書ける。clear(/clear 実行時)は前回状態を引き継ぐ意味がないので意図的に外している。
3. 動作確認
claude --debug
起動直後にデバッグログへ以下のような行が出れば注入成功。
[DEBUG] SessionStart hook matched (startup): session-start-context.sh
[DEBUG] additionalContext injected (342 chars)
セッションの一番最初のユーザーターンで、モデルが STATE.md の内容を「読んだ体」で応答してくれば実証できている。実際に「前回の続きから」とだけ聞いてみて、具体的な作業内容を返してくるかで確認するのが手っ取り早い。
ハマりどころ
① matcher を空にすると起動のたびに毎回発火する
{ "hooks": { "SessionStart": [{ "hooks": [{ "type": "command", "command": "..." }] }] } }
matcher キーを省略すると startup / resume / clear / compact の全種類にマッチする。/compact 実行のたびに STATE.md を読み込む処理が走ると、圧縮直後にまた同じコンテキストを積み増すことになり本末転倒。必ず matcher を明示する。
② CLAUDE.md との内容重複でトークンを二重に消費する
CLAUDE.md に既にプロジェクト概要を書いている場合、SessionStart hook でさらに似た内容を注入すると同じ情報が二重に context に載る。実測で、20 行程度の重複でも import 済み CLAUDE.md 込みで 500〜800 トークン程度の無駄が発生した(モデル・トークナイザーにより変動)。hook は「CLAUDE.md に書けない動的な情報」(直近のコミット・進行中タスクの状態など)に限定するのが良い。
③ hook がブロッキングでセッション起動が固まる
timeout を設定しないと、hook スクリプトが外部 API を叩くような重い処理をした場合にセッション起動自体が固まる。特に Notion や外部 DB から状態を取ってくる実装にすると、ネットワーク不調時に何十秒も起動が止まる。timeout は 3〜5 秒程度に短く設定し、失敗時は空の JSON({})を返して起動を優先させるフォールバックを必ず入れる。
timeout 3 curl -sf "https://example.com/api/state" || echo '{}'
背景・補足
additionalContext は Claude Code の会話履歴には残らず、モデルへの入力にだけ差し込まれる仕組みになっている(hook の実行結果自体がユーザー発言として記録されるわけではない)。そのため「セッションをまたいだ記憶」を作りたい場合、CLAUDE.md のような静的ファイルと SessionStart hook のような動的注入を使い分けるのが実用的な設計になる。静的な方針は CLAUDE.md、日々変わる状態は hook、という役割分担が崩れにくい。
まとめ
-
SessionStarthook は matcher(startup/resume/clear/compact)で発火条件を絞れる、絞らないと事故る -
additionalContextでモデルにだけ情報を渡せる、ユーザー発言としては残らない - CLAUDE.md と役割分担し、動的な状態だけを hook で注入するとトークンの無駄が減る
- 外部アクセスを伴う hook には必ず timeout とフォールバックを入れる