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

SessionStart hookでプロジェクトの文脈を毎回自動で渡す 日付・ブランチ・命名規則

1
Posted at

はじめに

Claude Codeで作業していると、「計画ファイルはどこに置くか」「レポートのファイル名はどう付けるか」といったプロジェクト固有の決まりを、毎回伝え直していることがあります。この記事では、SessionStart hookを使って、セッションの開始時と再開時、会話が要約(compact)されたときに、日付・ブランチ・命名規則を自動で文脈に渡す最小構成を書きます。

私たちの社内でも、計画やレポートの命名規則を同じ仕組みで渡しています。ただ、この記事のコードは説明用に新しく書いたもので、実際に使っている設定ファイルではありません。

途中で、手元で試して分かった少し意外な結果(hookがなくても日付とブランチは渡されていた)も書いています。設定だけ知りたい方は「1.」と「2.」だけ拾ってもらえれば十分です。

環境

macOS、Node.js v22.22.3、Claude Code 2.1.288。2026年10月4日に動作を確認しました。hookの挙動はバージョンによって変わる可能性があります。

この記事をざっくり図解

hookは、Claude Codeから標準入力でJSONを受け取り、標準出力に返したJSONの additionalContext が、そのセッションの文脈に加わります。

1. settings.jsonに登録する

プロジェクトの .claude/settings.json に書きます。

.claude/settings.json
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume|compact",
        "hooks": [
          {
            "type": "command",
            "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-context.cjs"
          }
        ]
      }
    ]
  }
}

matcher には、どのきっかけで動かすかを | 区切りで書きます。公式ドキュメントでは、SessionStartのきっかけとして startup(新しいセッション)、resume(--resume や --continue での再開)、clear(/clear)、compact(自動または手動の要約)、fork が挙げられています。今回は clear と fork を外しました。どこまで含めるかは、運用に合わせて決めてください。

命名規則などの値は、スクリプトに直接書かず、別のJSONに分けました。

.claude/project-context.json
{
  "timezone": "Asia/Tokyo",
  "planDir": "plans/{date}-{slug}/",
  "reportFile": "plans/reports/{type}-{date}-{slug}.md"
}

2. hookのスクリプト

外部パッケージは使っていません。

.claude/hooks/session-context.cjs
#!/usr/bin/env node
// SessionStart hook: 日付・ブランチ・命名規則をセッションの文脈として渡す(説明用の最小構成)
const { execSync } = require("node:child_process");
const fs = require("node:fs");
const path = require("node:path");

function readJson(file) {
  try {
    return JSON.parse(fs.readFileSync(file, "utf8"));
  } catch {
    return null;
  }
}

function currentBranch(cwd) {
  try {
    const out = execSync("git branch --show-current", { cwd, stdio: ["ignore", "pipe", "ignore"] });
    return out.toString().trim() || "(detached HEAD)";
  } catch {
    return "(gitリポジトリ外)";
  }
}

function today(timeZone) {
  // sv-SE は YYYY-MM-DD 形式で出る
  return new Intl.DateTimeFormat("sv-SE", { timeZone }).format(new Date());
}

const input = readJson(0) || {}; // hookの入力はstdinのJSON(0 = stdin)
const cwd = input.cwd || process.cwd();
// 設定がなくても、日付とブランチだけは渡す
const config = readJson(path.join(cwd, ".claude", "project-context.json")) || { timezone: "Asia/Tokyo" };
const date = today(config.timezone);

const lines = [`今日の日付: ${date} (${config.timezone})`, `現在のブランチ: ${currentBranch(cwd)}`];
if (config.planDir) lines.push(`計画の置き場所: ${config.planDir.replace("{date}", date)}`);
if (config.reportFile) lines.push(`レポートのファイル名: ${config.reportFile.replace("{date}", date)}`);
if (input.source === "compact") lines.unshift("(会話が要約されたため、プロジェクトの前提を再掲します)");

process.stdout.write(
  JSON.stringify({
    hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: lines.join("\n") },
  })
);

ポイントは3つです。

1つ目は、日付をタイムゾーン付きで作っていることです。サーバーやCI上で動かすと、マシンのタイムゾーンが日本時間とは限りません。ファイル名に日付を入れる運用では、ここがずれると前日の日付のファイルができてしまいます。

2つ目は、設定ファイルやgitがなくても止まらないことです。SessionStartのhookが失敗しても作業自体は始められますが、エラーを毎回見せられるのは煩わしいので、取れない値は「取れなかった」と書いて渡すようにしました。

3つ目は、source が compact のときに一行足していることです。要約のあとに前提が渡し直されたことが、文脈の上でも分かるようにするためです。

3. 動作を確かめる

まず、hookに渡されるJSONを手で作って、スクリプトだけを動かします。cwd を省くと、スクリプトは実行したディレクトリを使います。

$ cd demo-project
$ echo '{"source":"compact"}' | node .claude/hooks/session-context.cjs

出力の additionalContext を展開すると、次のとおりです。

(会話が要約されたため、プロジェクトの前提を再掲します)
今日の日付: 2026-10-04 (Asia/Tokyo)
現在のブランチ: feature/invoice-export
計画の置き場所: plans/2026-10-04-{slug}/
レポートのファイル名: plans/reports/{type}-2026-10-04-{slug}.md

次に、Claude Code本体で確かめます。claude -p で、渡された文脈だけを見て答えるように頼みました。

$ claude -p "セッション開始時に渡された文脈だけを見て、今日の日付、現在のブランチ、レポートのファイル名の規則を3行でそのまま答えてください。ツールは使わないでください。" --model haiku
今日の日付:2026-10-04 (Asia/Tokyo)

現在のブランチ:feature/invoice-export

レポートのファイル名:plans/reports/{type}-2026-10-04-{slug}.md

hookが返した値が、そのまま答えに出ています。

4. hookがなくても、日付とブランチは渡されていた

ここで比較のために、settings.json を外して同じ質問をしてみました。分からないものは「不明」と答えるよう、一文だけ足しています。

今日の日付: 2026-10-04

現在のブランチ: feature/invoice-export

レポートのファイル名の規則: 不明

日付とブランチは、hookがなくても答えられました。手元のバージョンでは、Claude Code側がセッションの開始時に、日付やgitの状態を最初から渡しているようです。正直なところ、この記事は「日付とブランチを渡すhook」として書き始めたので、ここは予想外でした。

この結果を見て、このhookの価値を考え直しました。hookでしか渡せないのは、プロジェクト固有の決まり(今回でいえば命名規則と、日付の書式・タイムゾーン)です。日付とブランチを残しているのは、命名規則に日付を埋め込むためと、compactのあとに今のブランチを渡し直すためです。会話の途中でブランチを切り替えた場合、開始時に渡された情報は古くなります。compactのタイミングでhookが動けば、その時点のブランチで上書きできます。

注意点と限界

文脈が長くなるほど、トークンを消費します。 additionalContextに書いた内容は、セッションの文脈の一部として扱われます。今回の出力は179文字ですが、規約やチェックリストを丸ごと流し込むような使い方をすると、その分だけ毎回の消費が増えます。ドキュメントを見た範囲では、additionalContextの長さの上限は見つけられませんでした。上限がないことを確かめたわけではないので、短く保つのが無難です。渡すのは「毎回必ず要る前提」だけに絞り、詳しい規約はファイルの場所を伝えて、必要なときに読ませる形にしています。

秘密情報を入れないこと。 hookの出力は文脈に入るので、APIキーや社外秘の設定値は書かないでください。設定用のJSONも、リポジトリに入れて困らない値だけにします。

途中の変化には追従しません。 SessionStartは開始時、再開時、compactのときにしか動きません。会話の途中でブランチを切り替えても、次のcompactまでは古いブランチ名のままです。毎回のプロンプトごとに最新の状態を渡したい場合は、UserPromptSubmit hookを使う方法もありますが、その分だけ毎ターン文脈が増えます。

hookが渡した内容を、モデルが必ず守るとは限りません。 命名規則を渡しても、それに従ったファイル名になるかは別の話です。名前が重要な場面では、作られたファイル名を別のチェックと組み合わせるほうが安全です。

最後に

SessionStart hookは、数十行のスクリプトで「毎回言っていること」を減らせます。ただ、試してみて分かったとおり、Claude Code側がすでに渡しているものもあります。まずはhookなしで何が分かっているかを確かめてから、足りないものだけを渡すのがよいと思います。

参考文献

VAON Việt Nam のエンジニアチームで、Claude Codeを使った開発の運用を記録しています。

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