0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code の SessionStart hook でセッション復帰時に前回の作業メモを自動注入する実装 ― startup/resume/clear の使い分けと3つのハマりどころ【2026】

0
Posted at

はじめに / 対象と前提

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

  • SessionStart hook は matcherstartup / 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、という役割分担が崩れにくい。

まとめ

  • SessionStart hook は matcher(startup / resume / clear / compact)で発火条件を絞れる、絞らないと事故る
  • additionalContext でモデルにだけ情報を渡せる、ユーザー発言としては残らない
  • CLAUDE.md と役割分担し、動的な状態だけを hook で注入するとトークンの無駄が減る
  • 外部アクセスを伴う hook には必ず timeout とフォールバックを入れる
0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?