Claude Codeを少し触ってみて、毎回手で同じ準備コマンドを叩くのに飽きてきた人向け
Claude Codeを使っていて、起動するたびに『cdして、git pullして、ファイル一覧を確認して…』と毎朝同じ準備コマンドを手で繰り返している場面で叩く。フックとしてシェルスクリプトを登録しておくと、その手順が毎セッション自動で走り、結果がAIに渡った状態で会話が始まる
Claude Codeを使い始めて毎日同じプロジェクトを触っていると、起動するたびに同じ準備動作を手で繰り返しているはず。「cd ~/projects/cooking-blog で移動して、git pull で最新に揃えて、ls drafts/ で書きかけのレシピを見て…」と、私も最初の3週間は毎朝これをやっていた。SessionStart はその「毎朝の儀式」を Claude Code が起動した瞬間に自動で流して、結果を AI が読める形で置いておくための仕組みです。
面白いのは、画面に出した文字列がそのまま AI のコンテキストに混ざる点。echo で「下書き中のレシピは bibimbap.md と ramen.md の2件」と出しておけば、最初のユーザープロンプトを送る前に Claude が既にその情報を知っている状態で会話が始まります。
噛み砕くと「キッチン開店前の仕込み係」
料理屋に例えると、SessionStart は「店を開ける前に冷蔵庫を確認して、今日の仕込みメモをカウンターに置いておく担当者」です。シェフ(Claude)が出社してきたとき、メモを読めば「今日は鶏肉が在庫薄、刺身は午前中の予約3件あり」みたいな状況がすぐ分かる。
逆に SessionStart を仕込んでいないと、シェフは出社するたびに毎回冷蔵庫を見て、予約表をめくって、と毎回同じ確認動作をする。それが「毎回 git pull を手で叩いている状態」と同じです。
画面に何が出るのか(実際の挙動)
言葉だけだと分かりづらいので、実際に SessionStart を仕込んだ Claude Code を起動したときの流れを見ます。フックとして「git pull して、下書き一覧を画面に出す」だけのシェルスクリプトを登録した想定です。
$ claude
[SessionStart hook running...]
draft一覧:
bibimbap.md
ramen.md
napolitan.md
> こんにちは。今日の作業を始めましょう。
「draft一覧:」以下の3行は、フックが画面に出した文字列。これがそのまま AI に渡されている。だから次に私が「下書きで止まってるビビンバを仕上げよう」と打つと、Claude は既に bibimbap.md の存在を知っていて、いきなり中身を読み始めます。
これがミソ。draft一覧 を伝える会話のラリーが消える。
「料理ブログ cooking-blog」を例に、4ステップで仕込む
Hugo(静的サイト生成ツール)で作っている料理ブログを題材に、SessionStart を実際に動かしてみます。やりたいのは3つ。「作業フォルダに移動」「最新の変更を取り込む」「下書き一覧を画面に出す」。
ステップ1: フック本体のシェルスクリプトを書く
プロジェクトの直下に .claude/hooks/ フォルダを作って、session-start.sh という名前で次の内容を保存します。
#!/bin/bash
cd ~/projects/cooking-blog
git pull --quiet
echo "===== cooking-blog セッション準備 ====="
echo "現在ブランチ: $(git branch --show-current)"
echo ""
echo "下書き中のレシピ:"
ls drafts/ 2>/dev/null || echo "(drafts フォルダが空)"
保存したら忘れずに実行できる状態にします。
$ chmod +x .claude/hooks/session-start.sh
ステップ2: settings.json にフックを登録する
同じプロジェクトの .claude/settings.json を開いて、hooks.SessionStart の項目を足します。matcher を書かなければ、起動・再開・/clear 後・会話圧縮後の全パターンで走ります。
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh"
}
]
}
]
}
}
ステップ3: Claude Code を起動して動作を見る
cooking-blog フォルダ直下で claude を叩くと、最初のプロンプトを打つ前にフックが走って、画面に下書き一覧が表示されます。
$ cd ~/projects/cooking-blog
$ claude
===== cooking-blog セッション準備 =====
現在ブランチ: main
下書き中のレシピ:
bibimbap.md
napolitan.md
ramen.md
>
ステップ4: AI が情報を覚えているか確認する
そのまま「いま下書きで止まっているレシピを1つ仕上げたい。どれが良さそう?」と打つと、Claude は3つのファイル名を既に知っている前提で答えてきます。ls drafts/ を AI 側に打たせる必要がない。
ここで初心者がやりがちな勘違いがあります。「画面に出てる文字は AI に渡らないんじゃないの?」と思いがちですが、SessionStart については逆。**標準出力に出した文字がそのまま AI のコンテキストに追加される**のがこのフックの一番の特徴です。
仕組みをもう少し正確に
SessionStart は4種類のきっかけで発火します。matcher でどのきっかけだけに絞るかを選べる。公式が定義している値は以下の通り。
| matcher の値 | いつ発火するか |
|---|---|
startup |
新しくセッションを始めたとき(普通の claude 起動) |
resume |
--resume や --continue、/resume で前の会話を再開したとき |
clear |
会話の途中で /clear を叩いてリセットしたとき |
compact |
会話が長くなりすぎて自動圧縮(compact)が走ったとき、または手動で圧縮したとき |
フックに渡される入力データ(JSON形式)には次のフィールドが入っています。
{
"session_id": "abc123",
"transcript_path": "/Users/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/me/projects/cooking-blog",
"hook_event_name": "SessionStart",
"source": "startup",
"model": "claude-sonnet-4-6"
}
source の値が「今回はどのきっかけで起動したか」を教えてくれる。matcher なしで全パターンで走らせつつ、スクリプト側で source を見て分岐する書き方もあります。
もう1つ覚えておきたい挙動。フック側で echo で画面に出すだけでも AI に渡せますが、もっと構造的に渡したい場合は次の形のJSONを画面に出します。
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "現在ブランチ: main\n下書き: bibimbap.md, ramen.md"
}
}
この additionalContext に書いた文字列が、最初のプロンプトの直前に「システムリマインダー」として AI に挿入されます。チャット欄には表示されないけど、AI の判断には影響する。
つまり SessionStart は何をしてくれるのか
- やってくれる: セッション開始時に毎回同じ準備コマンドを自動実行し、結果を AI に渡す(
git pull、ファイル一覧、ブランチ確認など) - やってくれる: 起動・再開・
/clear後・会話圧縮後の4タイミングそれぞれを区別して処理を変える - やってくれない: セッション終了時の片付け(それは別の SessionEnd フックの仕事)
- やってくれない: ツール実行の前後に走らせる処理(PreToolUse / PostToolUse の担当)
- 意味が薄い場面: 1人で1つのプロジェクトしか触らず、毎回同じ準備が要らない使い方
使いどころ3シナリオ
シナリオ1: 料理ブログで「下書き棚卸し」を毎朝自動化
Hugo製の料理ブログを毎日少しずつ書いていると、「今どのレシピが下書きで止まっているか」を毎回 ls drafts/ で確認するのが地味にめんどくさい。SessionStart に ls drafts/ && cat drafts/*.md | head -20 みたいに仕込んでおけば、起動と同時に「下書き一覧 + 各下書きの先頭20行」が画面と AI コンテキストに出ます。「napolitan の続きを書こう」と言うだけで、Claude が冒頭を覚えた状態で続きを書き始める。
シナリオ2: 複数人プロジェクトで「最新の同期」を強制
料理ブログをパートナーと2人で書いている場合、作業を始める前に相手の変更を取り込んでいないと、コミット時に衝突する。SessionStart に git pull --rebase を仕込んでおけば、「git pull 忘れて作業始めた問題」を構造的に潰せる。私がレシピを書き始める前に Claude も「最新のブランチ状態」を把握できる状態になる。
シナリオ3: 起動時の設定値(環境変数)を毎回読み直す
Hugo は HUGO_BASEURL という設定値で「公開時のURL」を切り替える。本番用と下書きプレビュー用で値が違う。SessionStart で echo "export HUGO_BASEURL=https://cooking-blog.local" >> "$CLAUDE_ENV_FILE" と書いておくと、そのセッション中に Claude が hugo server を叩いたときに正しい設定値が効いた状態で起動する。設定値を毎回手で読み込ませる手間が消えます。
初心者が踏みやすい落とし穴
- 画面に大量出力するとコンテキストを汚す。
git pullのログを--quietなしで流したり、ls -laでファイルを200個出したりすると、その全部が AI に渡って、肝心の本題を考える余地が削られる。出すのは「AI に知っておいてほしい要点」だけに絞る。 /clearでも発火するのを忘れる。会話の途中で/clearを叩くと、SessionStart がもう一度走る。「起動時に1回だけやればいい処理」を書くと、/clearのたびに走って二重実行になる。matcher: "startup"で絞るか、スクリプト側でsourceを見て判断する。- SessionEnd と対になっていない。SessionStart で
npm run devのような長時間プロセスを起動しても、終了時に止める仕組みは別途 SessionEnd フックを書かないといけない。バックグラウンドで走らせっぱなしにしない。 cwdはセッション開始時の場所で固定。起動した瞬間のフォルダ位置がcwdとして渡される。会話中に Claude がcdで別フォルダに移動しても、cwdの値は更新されない。スクリプト側で「今いるフォルダ」を知りたいときはpwdを都度叩き直す。- 10分以上かかる処理は止められる。
command型のフックには10分のタイムアウトがある。重いgit cloneやnpm installを SessionStart に置くと、終わる前に切られる。重い処理は別の手動コマンドに逃がす。 --bare起動ではフックがスキップされる。テスト用にclaude --bareで起動するとフックは動かない。「あれ、フックが走らない」と慌てる前に、起動コマンドの後ろに何を書き足したかを確認する。- ツール実行の判定ロジックを使い回せない。SessionStart には PreToolUse のような
tool_nameやtool_inputが渡されない。「Bash ツールが叩かれたら〜」みたいな判定は SessionStart では成立しない。きっかけが違うフックだと割り切る。
書き方
.claude/settings.json の hooks.SessionStart に登録する。matcher を書かなければ全きっかけ(起動・再開・/clear後・compact後)で走る。matcher に startup / resume / clear / compact のどれかを書けば、そのきっかけだけに絞れる
やってみるとこうなる
入力
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh"
}
]
}
]
}
}
出力例
$ claude
===== cooking-blog セッション準備 =====
現在ブランチ: main
下書き中のレシピ:
bibimbap.md
napolitan.md
ramen.md
>
(この『下書き中のレシピ』3行がそのままAIのコンテキストに追加されるので、次のプロンプトで『下書きを仕上げよう』と言えばClaudeが3つのファイル名を既に把握した状態で答えてくる)
このページに出てきた言葉
- セッション
- Claude Codeを起動してから終了する(または<code>/clear</code>で会話をリセットする)までの1まとまりの会話
- フック
- 「○○のタイミングで自動で△△を実行する」という設定。料理ブログで言えば「記事を公開したらTwitterに自動通知する」発想と同じ
- コンテキスト
- AIが現在の会話で「覚えている」内容のかたまり。ここに情報を入れておくと前提として使える
- matcher
- 「どのきっかけで発火させるか」を絞り込む指定。<code>startup</code> / <code>resume</code> / <code>clear</code> / <code>compact</code> のどれか。書かなければ全部で走る
- 標準出力
- プログラムが「画面にこれを出したい」と書き出す出口。<code>echo</code>で出した文字列はここに流れる
- settings.json
- Claude Codeの設定をまとめて書いておくJSON形式のファイル。プロジェクトごとの設定はそのプロジェクトの<code>.claude/settings.json</code>に書く
- シェルスクリプト
- ターミナルで打つコマンドをファイルに書いて1発でまとめて実行できるようにしたもの。<code>.sh</code>という拡張子で保存することが多い
- ターミナル
- 黒い画面で文字のコマンドを打ち込む画面。Windowsだと「コマンドプロンプト」「PowerShell」、Macだと「ターミナル」アプリ
- $CLAUDE_PROJECT_DIR
- Claude Codeが起動時に自動で用意してくれる、プロジェクトのルートフォルダのパス
- $CLAUDE_ENV_FILE
- SessionStart などのフック向けにClaude Codeが用意してくれる「設定値を書き足すための一時ファイル」のパス。<code>export ○○=△△</code>の形で書き足すと、そのセッション中ずっと有効になる
- compact(会話圧縮)
- 会話が長くなってAIが覚えきれなくなる前に、過去の会話を要約して短くする処理。自動と手動(<code>/compact</code>)の両方がある
- git pull
- 共有のリポジトリ(GitHub等)から手元のパソコンに「最新の変更」をダウンロードして取り込む操作
- git clone
- リモートのリポジトリ(GitHub等)から、プロジェクトのファイル一式と変更履歴をまるごと手元のパソコンにコピーしてくる操作。最初の1回だけ叩く
- システムリマインダー
- AIに「これを前提として覚えておいて」と裏で渡す指示書きの一種。チャット画面には出ないがAIの応答内容には影響する
- JSON
- データを<code>{ "key": "value" }</code>の形で書く決まり。設定ファイルやプログラム同士のデータやり取りでよく使われる
- cwd
- current working directory の略。Claude Code を起動したとき『どのフォルダにいたか』のパス。フック側で「今プロジェクトのどこから起動されたか」を判定するときに使う
- session_id
- このセッション固有のID文字列。1回の起動 = 1つの session_id。ログを後から追跡したいときの目印
- transcript_path
- このセッションの会話ログが保存される .jsonl ファイルのパス。フック側で「過去の会話を振り返って何かしたい」ときに読みに行く先
- hook_event_name
- 今呼ばれているフックの種類名。SessionStart の場合は常に<code>"SessionStart"</code>という文字列が入る
- pwd
- ターミナルで「いまどのフォルダにいるか」を表示するコマンド。<code>cwd</code> がセッション開始時のフォルダ固定なのに対し、<code>pwd</code> は今この瞬間のフォルダを返す
- --bare
- <code>claude --bare</code> のように起動コマンドの後ろに書き足すと、フックや一部の自動処理を全部スキップして最小構成で起動する指定。動作確認用
- タイムアウト
- 「ここまでに終わらなかったら強制的に止める」という打ち切り時間の設定。SessionStart の <code>command</code> フックは10分が上限
- npm install
- JavaScript 系プロジェクトで、必要なライブラリ一式をダウンロードしてくる処理(数分かかることが多い)
- npm run dev
- JavaScript 系プロジェクトで、開発用のサーバーを立ち上げて動かしっぱなしにする処理
- tool_name / tool_input
- PreToolUse などの別フックに渡される入力値。「Claude がいま何のツールを使おうとしているか」「そのツールに何を渡そうとしているか」を表すフィールド名。SessionStart のフックには渡されない