Claude Codeのプラグイン推奨をSessionStartフックで自動化する
Claude CodeのSessionStartフックは起動ディレクトリを読み取り、そのエリアの担当チームが管理するプラグインを最初の返信で案内できます。モノレポで規約集約が効かなくなったときの入り口です。
Claude Codeのプラグイン推奨をSessionStartフックで自動化する仕組みとは
Claude CodeのSessionStartフックとは、セッション開始時に発火し、標準出力に書いたプレーンテキストをClaudeの会話に最初のプロンプトより前のコンテキストとして追加できる仕組みです。この仕組みを使うと、Claudeを起動したディレクトリを読み取り、そのエリアを管理するチームが用意したプラグインを最初の返信でチームメイトに案内させられます。
対象になるのは、規約やスキルをプラグインに集約している大規模リポジトリです。プラグインとして配布された規約は、そのプラグインを知らない人には見えません。初めて触るパッケージでclaudeを起動したとき、そのエリアのオーナーがどのプラグインを前提にしているかを知る手がかりが何もない、という状態が起きます。
なぜ規約をCLAUDE.mdからプラグインへ移すとこの問題が起きるか
ディレクトリごとにCLAUDE.mdを置く方式は、コードベースが育つにつれて統治が難しくなります。規約が古くなる、ルートのCLAUDE.mdを誰も更新しなくなる、といった劣化が起きやすいためです。解決の方向は、常時ロードされるCLAUDE.mdから、必要なときだけ読み込まれる仕組みへ規約を移すことです。具体的には、Claudeがタスクに関連すると判断したときだけ読み込まれるSkills、プラットフォームチームが一元管理できるPlugins、既存のコード検索やRAGインデックスをツールとして公開するMCPサーバーの3つが選択肢になります。
規約をプラグインへ移した瞬間に新しい問題が生まれます。オンボーディング時点で全員が把握していた「このパッケージではこのCLAUDE.mdを読む」という前提が、「このパッケージではこのプラグインを有効化する」という前提に変わり、しかもその前提はプラグインを知っている人にしか見えません。SessionStartフックは、この見えなくなった前提を起動時に可視化する役割を担います。
SessionStartフックで起動ディレクトリからプラグインを引き当てる
SessionStartフックは、共通の入力フィールドに加えてsource(startup / resume / clear / compact / forkのいずれか)を受け取ります。フックのスクリプトは標準入力からこのJSONを読み、cwd(起動時のカレントディレクトリ)を取り出します。
やることは3つです。①フック入力からcwdを読む、②リポジトリにコミットされたパスとプラグイン名の対応表を引く、③一致したプラグイン名をClaudeへの追加コンテキストとして返す、という流れです。対応表はJSONで持たせておくと、パッケージが増えたときの追記が楽になります。
{
"packages/api": "api-conventions@internal-tools",
"packages/web": "web-conventions@internal-tools",
"packages/mobile": "mobile-conventions@internal-tools"
}このJSONを.claude/plugin-map.jsonのようなパスでリポジトリに置き、フックのスクリプトから読みます。
#!/bin/bash
INPUT=$(cat)
CWD=$(echo "$INPUT" | jq -r '.cwd')
MAP_FILE="$(git rev-parse --show-toplevel 2>/dev/null)/.claude/plugin-map.json"
if [ -f "$MAP_FILE" ]; then
MATCH=$(jq -r --arg cwd "$CWD" '
to_entries[] | select($cwd | contains(.key)) | .value
' "$MAP_FILE" | head -n1)
if [ -n "$MATCH" ]; then
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"SessionStart\",\"additionalContext\":\"このディレクトリの担当チームは ${MATCH} プラグインを前提にしています。未インストールなら最初の返信で案内してください。\"}}"
fi
fiこのフックはリポジトリ全員に効かせたいので、プロジェクトの.claude/settings.jsonに登録してコミットします。個人の好みで別の案内を足したい場合は.claude/settings.local.jsonに、組織として絶対に外させたくない案内なら管理設定に置くという使い分けです。プロジェクト設定は共有され、ローカル設定は各自のマシンに閉じる、という基本原則はこのフックにもそのまま当てはまります。
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/recommend-plugin.sh"
}
]
}
]
}
}フックそのものをプラグインとして配布する
ここまでの例は.claude/settings.jsonに直接フックを書く方法でしたが、フックはPluginsのhooks/hooks.jsonにも同じ形式で書けます。社内の共通ツールキットをプラグインとして配布している組織なら、プラグイン推奨のロジック自体をそのプラグインへ同梱するという選択肢が生まれます。プラグインを更新すれば対応表も一緒に更新される形になり、フックの配布をリポジトリのコミット履歴とは別のライフサイクルで管理できます。
どちらの置き場所を選ぶかは、更新の主体で決まります。リポジトリのコミットで管理したいなら.claude/settings.json、複数リポジトリにまたがる共通ロジックとしてバージョン管理したいならプラグイン、という切り分けが基本です。
効かせる範囲を絞る — matcherの使い分け
matcherにはstartup(新規セッション)、resume(--resume・--continue・/resume)、clear(/clear後)、compact(要約後)、fork(セッションの分岐)の5種類があります。プラグイン推奨は「初めてそのディレクトリで作業を始めた瞬間」に効かせたい情報なので、startupだけに絞るのが基本です。resumeまで含めると、同じセッションを再開するたびに同じ案内が繰り返し流れ、会話のノイズになります。
additionalContextで返した文字列は、セッションのトランスクリプトに保存されます。会話の途中で読み返されることはあっても、Claudeが毎回それを読者に見せるわけではない点は理解しておきます。実際に案内として表示させたいなら、プロンプト内の文言で「最初の返信で伝えてください」のように明示する必要があります。
SessionStartフックはsourceのほかに、任意でmodel(有効なモデル名)・agent_type(claude --agent <name>で起動した場合のエージェント名)・session_title(すでにタイトルが設定済みなら現在のタイトル)も受け取ります。特定のカスタムエージェントで起動したときだけ案内を出したい、といった条件分岐が必要な場合はこれらのフィールドで絞り込めます。フック自体はtype: "command"かtype: "mcp_tool"のみ対応で、セッションのたびに必ず実行されるため、処理は軽く保ちます。
関連する自動化との使い分け早見表
同じ「起動時に情報を渡す」目的でも、手段によって向き不向きが分かれます。
| 手段 | 向いているケース | 効かないケース |
|---|---|---|
CLAUDE.md(常時ロード) | 向いているケース静的で更新頻度の低い規約 | 効かないケース起動ディレクトリごとに変わる動的な案内 |
| Skills(タスク関連時に自動ロード) | 向いているケース特定の作業(テスト・マイグレーション等)に紐づく知識 | 効かないケース「このプラグインを使ってください」という導線そのもの |
| SessionStartフック | 向いているケース起動ディレクトリに応じた動的な案内・環境変数の設定 | 効かないケースチーム全員に強制したい設定(フックは案内するだけで強制しない) |
enabledPlugins(managed設定) | 向いているケース組織として必ず有効化したいプラグイン | 効かないケース個々のプラグインの使い方を都度説明したい場面 |
SessionStartフックは「気づかせる」役割に向いていて、「強制する」役割には向きません。プラグインを全員に必ず使わせたいなら、enabledPluginsをmanaged設定に置いて強制有効化する構成と組み合わせます。
よくあるつまずき
フックが発火しない場合、matcherをstartupに絞っていると/clearや--resumeでは動きません。意図したsourceでフックが呼ばれているかを、まずclaude --debug-file <path>で書き出したログで確認します。ログにはマッチしたフック名・終了コード・標準出力と標準エラーの全文が残るため、対応表の読み込みに失敗しているのか、JSON出力の形式が誤っているのかを切り分けられます。より詳細なマッチング情報が要る場合はCLAUDE_CODE_DEBUG_LOG_LEVEL=verboseを設定します。
案内文がClaudeの返信に出てこない場合、additionalContextはコンテキストに追加されるだけで、Claudeが必ずそれを最初の発言に含めるとは限りません。案内文の中に「最初の返信で伝えてください」のような指示を含めておくと再現性が上がります。
対応表のパスがマッチしない場合、cwdは起動時のディレクトリであり、その後cdした先までは追跡しません。サブディレクトリへ移動してから作業する運用が多いなら、CwdChangedイベントと組み合わせて再評価する構成を検討します。
フックが重くてセッション起動が遅い場合、SessionStartフックは毎セッション必ず実行されるため、外部APIへの問い合わせのような重い処理は避け、リポジトリ内のファイル参照だけで完結させます。
よくある質問
この仕組みはプラグインのインストールまで自動化しますか
しません。SessionStartフックが返すのは案内テキストだけです。実際のインストールは/plugin installをClaudeに実行させるか、enabledPluginsへの追加を別途行う必要があります。
モノレポ以外でも使えますか
使えます。単一の大規模ツリーでもディレクトリ構成がサブシステムごとに分かれていれば同じ対応表の考え方が成立します。パッケージ境界ではなく機能領域の境界で対応表を作ります。
複数のSessionStartフックを同時に登録できますか
できます。マッチしたフックはすべて並列に実行されるので、プラグイン推奨のフックと、たとえば直近のgitログを表示するフックを別々に登録しても構いません。同じハンドラを複数の設定ファイルに重複して書いた場合は1回だけ実行されます。
対応表が古くなって案内が間違っていたらどうなりますか
フックは案内を出すだけなので、間違った案内が出ても実害は「不要なプラグインをすすめられる」程度です。ただし信頼を失うと読まれなくなるため、対応表の更新はプラグイン追加のレビュープロセスに組み込んでおくのが安全です。
案内をチーム全員ではなく一部のディレクトリだけに出したい場合は
対応表に載っていないパスではフックが何も出力しない構成にしておけば、案内が必要なディレクトリだけに絞れます。上のスクリプト例も、一致がなければ何も出力しません。
案内文を長くしすぎると何が起きますか
additionalContextはトークンを消費してセッションのコンテキストに常駐します。案内は「プラグイン名+一言」程度に留め、規約の詳細そのものはプラグインが読み込むスキルやドキュメント側に持たせるほうが、起動のたびに払うコストを小さく保てます。
まとめ
Claude Codeのプラグイン推奨をSessionStartフックで自動化する狙いは、規約をCLAUDE.mdからプラグインへ移した組織で失われがちな「このエリアはどのプラグイン前提か」という情報を、起動した瞬間に取り戻すことです。cwdを読み、リポジトリにコミットした対応表と突き合わせ、additionalContextで案内する、という3ステップの組み合わせだけで動きます。強制ではなく案内である点を踏まえ、必須にしたいプラグインはenabledPluginsのmanaged設定と併用します。