2
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 で前回の作業メモと git 状態をセッション開始時に自動注入する実装手順 — /compact 後に消える・export が Bash に効かない・exit 2 で止まらない、3つのハマりどころ【2026】

2
Posted at

はじめに / 対象と前提

Claude Code を毎日使っていると、セッションを開くたびに「前回どこまでやったっけ」「今どのブランチだっけ」を説明し直すのが地味に面倒になる。CLAUDE.md に書くほど恒久的な情報ではなく、その日・その瞬間の状態 を毎回渡したい。

これを自動化するのが SessionStart hook だ。セッション開始時にスクリプトを走らせ、その出力を Claude のコンテキストに差し込める。

この記事は、SessionStart hook で「前回の引き継ぎメモ」と「git の状態」を自動注入する実装手順と、自分が実際にハマった3点をまとめたもの。

想定読者

  • Claude Code を日常的に使っていて、hooks を一度は触ったことがある人
  • 長時間・複数セッションにまたがる作業で、毎回の状況説明を省きたい人

環境

  • Claude Code v2.1 系(2026年10月時点)
  • macOS 15 / Python 3.13
  • hooks の設定は .claude/settings.json(プロジェクト単位)

TL;DR

  • SessionStart hook は exit 0 で stdout に出した内容がそのままコンテキストに入る。JSON の additionalContext でも渡せる
  • matcher は startup / resume / clear / compact の4種。startup だけにすると /compact や /clear の後に情報が消える
  • hook 内の export は Bash ツールに引き継がれない。環境変数は CLAUDE_ENV_FILE に追記する

手順 / 動かし方

1. 設定ファイルに hook を登録する

.claude/settings.json に以下を書く。

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume|clear|compact",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/session_start.py\"",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

$CLAUDE_PROJECT_DIR はプロジェクトのルートを指す。相対パスで書くと、サブディレクトリから起動したときにスクリプトが見つからず静かに失敗するので、絶対パスで解決させておく。

2. 注入スクリプトを書く

.claude/hooks/session_start.py:

#!/usr/bin/env python3
import json
import os
import subprocess
import sys
from pathlib import Path

MEMO = Path(os.environ.get("CLAUDE_PROJECT_DIR", ".")) / ".claude" / "handoff.md"
MAX_MEMO_CHARS = 3000  # 注入しすぎるとコンテキストを圧迫する


def git(*args: str) -> str:
    try:
        r = subprocess.run(["git", *args], capture_output=True,
                           text=True, timeout=3)
        return r.stdout.strip()
    except (subprocess.TimeoutExpired, FileNotFoundError):
        return "(取得失敗)"


def main() -> None:
    payload = json.load(sys.stdin)
    source = payload.get("source", "startup")

    parts = [f"## セッション開始情報(source={source})"]
    parts.append(f"- ブランチ: {git('branch', '--show-current')}")
    status = git("status", "--short")
    parts.append("- 未コミット変更:\n" + (status or "なし"))
    parts.append("- 直近コミット:\n" + git("log", "--oneline", "-5"))

    if MEMO.exists():
        memo = MEMO.read_text(encoding="utf-8")[:MAX_MEMO_CHARS]
        parts.append("## 前回の引き継ぎメモ\n" + memo)

    out = {
        "hookSpecificOutput": {
            "hookEventName": "SessionStart",
            "additionalContext": "\n".join(parts),
        }
    }
    print(json.dumps(out, ensure_ascii=False))


if __name__ == "__main__":
    main()

stdin には session_id cwd hook_event_name source などを含む JSON が渡ってくる。source を見れば、新規起動なのか /compact 後なのかで出し分けもできる。

3. 動作確認

chmod +x .claude/hooks/session_start.py
echo '{"source":"startup"}' | CLAUDE_PROJECT_DIR=$PWD python3 .claude/hooks/session_start.py

JSON が1行で出れば OK。続いて claude を起動し、最初に「今のブランチと前回のメモを要約して」と聞くと、ファイルを読みに行かずに即答する。claude --debug で起動すると hook の実行ログも確認できる。

ハマりどころ

ハマり1: /compact した瞬間に引き継ぎ情報が消える

症状:起動直後は前回メモを把握しているのに、長い作業で自動圧縮が走った後、急に「どのタスクでしたっけ?」と言い出す。

原因:最初は matcher を "startup" だけにしていた。SessionStart は 圧縮後(compact)や /clear 後にも発火する が、matcher に含めていないと hook が走らない。圧縮で要約された会話からは、注入した細かい情報が落ちやすい。

回避策:matcher を startup|resume|clear|compact にする(matcher を省略しても全種で動く)。圧縮後は特に「今どこまで終わったか」が重要なので、source == "compact" のときだけメモの比重を上げるのもアリ。

ハマり2: hook 内で export した環境変数が Bash ツールで見えない

症状:hook で export API_BASE=... しておけば Claude が叩くコマンドでも使えると思ったら、echo $API_BASE が空。

原因:hook は独立した子プロセスで動くので、そこで export しても親(Claude Code 本体)や後続の Bash ツールには伝播しない。

回避策:SessionStart hook にだけ渡される CLAUDE_ENV_FILE というパスに、export 文を 追記 する。ここに書いた内容は以降の Bash ツール実行時に読み込まれる。

env_file = os.environ.get("CLAUDE_ENV_FILE")
if env_file:
    with open(env_file, "a", encoding="utf-8") as f:
        f.write("export API_BASE=http://localhost:8080\n")

上書き("w")にすると、他の hook が書いた行を消してしまうので必ず "a" で。秘密情報はここに直書きせず、既存のシェル設定やシークレット管理から読む形にしておく。

ハマり3: exit 2 で「止めた」つもりがセッションは普通に始まる

症状:「main ブランチで直接作業していたら警告して止めたい」と思い、PreToolUse と同じ感覚で exit 2 を返した。が、セッションは何事もなく始まり、Claude も警告を知らない。

原因:SessionStart は ブロックできないイベント。exit 2 の stderr はユーザーの画面に出るだけで Claude には渡らない。さらに JSON 出力は exit 0 のときしか解釈されないので、exit 2 にした時点で additionalContext も捨てられる。

回避策:止めるのは諦めて、exit 0 のまま注意文を注入する。

if git("branch", "--show-current") in ("main", "master"):
    parts.insert(0, "**警告: 既定ブランチ上にいる。作業前に必ずブランチを切ること。**")

本当に操作を止めたいなら、ブロックは PreToolUse hook の担当にする。

背景・補足:何を注入するか

自分は無人で長時間回す自律システム(完全自律実装システム)を運用していて、エージェントが作業終了時に handoff.md へ「やったこと / 次にやること / 未解決の問題」を書く運用にしている。次のセッションはそれを SessionStart で受け取るので、人間が介在しなくても作業が途切れずつながる。

注意点は量。git log や diff を丸ごと流し込むと、毎セッション数千トークンを消費する。自分は「直近5コミット + short status + メモ3000字まで」に絞っている。また hook が遅いとセッション開始そのものが待たされるので、git コマンドにも個別のタイムアウトを付けた。

まとめ

  • SessionStart hook は exit 0 + stdout(または additionalContext)でコンテキストを注入できる
  • matcher は compact / clear も含めないと、圧縮後に引き継ぎ情報が消える
  • 環境変数は export ではなく CLAUDE_ENV_FILE へ追記する
  • SessionStart はブロック不可。exit 2 は使わず、注意文の注入で対応する
  • 注入量とスクリプトの実行時間は意識して絞る
2
0
0

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
2
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?