面倒くさがり屋の私としては、「次はあのプロジェクトの修正を」と思ったとき、Claude Code にリポジトリを毎回伝えるのが地味にストレスでした。
/exitしてcd してからclaudeコマンドを叩くか、会話の中でリポジトリのパスを手打ちするか。まあ、そのようにやれば言い訳なのですが、そこは根っからの面倒くさがりが頭をもたげてしまいます。プロジェクトが増えるほどこの摩擦は積み重なりますし、何より毎回やるのがどうにも性に合わない。
この記事では、UserPromptSubmit フックと additionalContext を使って、プロンプトにプロジェクト名を書くだけでリポジトリパスを自動的に伝える仕組みを作る方法を紹介します。
完成イメージ
ユーザー: 受発注システムのあのバグを確認して
↓ フックが自動的に additionalContext を注入
Claude: [受発注システムのパス /path/to/order-system を認識した上で回答]
会話の中でプロジェクト名を自然に出すだけで、Claude がそのリポジトリを意識した回答をしてくれます。cd も明示的なパス指定も不要です。
Claude Code のフックとは
Claude Code には「フック(hooks)」という仕組みがあります。特定のイベントが発生したときに自動でスクリプトを実行し、その結果を Claude の動作に反映できます。
フックの種類はいくつかありますが、今回使うのは UserPromptSubmit です。名前の通り、ユーザーがメッセージを送信した直後に実行されます。
フックの設定場所
~/.claude/settings.json に登録します。
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/repo_switcher.py"
}
]
}
]
}
}
matcher が空文字の場合、すべてのプロンプトに対してフックが実行されます。
フックスクリプトの入出力
フックスクリプトは標準入力から JSON を受け取り、標準出力に JSON を返します。
入力(stdin):
{
"prompt": "ユーザーが送信したテキスト",
"cwd": "/current/working/directory",
"session_id": "..."
}
出力(stdout)で additionalContext を返す:
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "Claudeに伝えたい追加情報"
}
}
additionalContext に文字列を入れると、Claude の処理前にシステムプロンプト的な形で差し込まれます。ここにリポジトリパスを入れることで、Claude がそのリポジトリを前提とした回答をするようになります。
スクリプトの全体像
#!/usr/bin/env python3
"""
UserPromptSubmit フック: プロンプト内容からリポジトリを推定し、
additionalContext でパスを Claude に伝える。
"""
import json
import sys
import re
REPOS = {
"受発注システム": "/path/to/order-system",
"order-system": "/path/to/order-system",
"社内ポータル": "/path/to/portal",
"ブログ": "/path/to/blog",
# 必要なだけ追加
}
LIST_TRIGGERS = [
"リポジトリ一覧", "repo一覧", "レポ一覧",
"リポジトリを選", "リポジトリ選択", "どのリポジトリ",
"プロジェクト一覧", "プロジェクトを選",
]
UNIQUE_REPOS = {}
for k, v in REPOS.items():
if v not in UNIQUE_REPOS.values():
UNIQUE_REPOS[k] = v
def is_list_request(prompt: str) -> bool:
return any(t in prompt for t in LIST_TRIGGERS)
def find_repo(prompt: str) -> tuple[str, str] | None:
for keyword in sorted(REPOS.keys(), key=len, reverse=True):
pattern = re.compile(re.escape(keyword), re.IGNORECASE)
if pattern.search(prompt):
return keyword, REPOS[keyword]
return None
def build_repo_list() -> str:
lines = []
for i, (name, path) in enumerate(UNIQUE_REPOS.items(), 1):
lines.append(f"{i}. {name} → {path}")
return "\n".join(lines)
def main():
try:
data = json.load(sys.stdin)
except Exception:
sys.exit(0)
prompt = data.get("prompt", "")
if is_list_request(prompt):
repo_list = build_repo_list()
output = {
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": (
"[repo-switcher] ユーザーがリポジトリ一覧・選択を求めています。\n"
"以下のリポジトリ一覧をAskUserQuestionツールで選択メニューとして表示してください。"
"選択肢が5個を超える場合は代表的なものを最大4個に絞り、残りは「その他」としてください。"
"ユーザーが選んだリポジトリのパスを作業ディレクトリとして以降の会話で使用してください。\n\n"
f"リポジトリ一覧:\n{repo_list}"
)
}
}
print(json.dumps(output))
return
result = find_repo(prompt)
if result:
keyword, path = result
output = {
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": (
f"[repo-switcher] プロンプトに「{keyword}」が含まれています。"
f"対応するリポジトリパスは: {path}\n"
f"このリポジトリで作業する場合は、Bashコマンドで `cd '{path}'` を先頭に付けるか、"
f"ファイルパスを絶対パスで指定してください。"
)
}
}
print(json.dumps(output))
if __name__ == "__main__":
main()
順にポイントを説明します。
ポイント1:REPOS 辞書の設計
REPOS = {
"受発注システム": "/path/to/order-system",
"order-system": "/path/to/order-system",
"社内ポータル": "/path/to/portal",
"ブログ": "/path/to/blog",
}
同じパスを複数のキーワードで登録できます。これにより、正式なリポジトリ名でも日本語の通称でも引っかかるようになります。「受発注システム」と「order-system」が同じパスを指していても問題ありません。
UNIQUE_REPOS は重複パスを除いた表示用の辞書で、一覧表示のときに同じプロジェクトが2回出ないようにするためのものです。
ポイント2:長い順でマッチさせる
def find_repo(prompt: str) -> tuple[str, str] | None:
for keyword in sorted(REPOS.keys(), key=len, reverse=True):
...
キーワードを長い順で試すのが重要です。たとえば「portal-admin」と「portal」が両方登録してある場合、短い方から試すと「portal」が先に引っかかって間違ったパスを返してしまいます。長い方から試すことで、より具体的なキーワードが優先されます。
ポイント3:一覧表示トリガー
LIST_TRIGGERS = [
"リポジトリ一覧", "repo一覧", "どのリポジトリ",
"プロジェクト一覧", ...
]
「リポジトリ一覧を見せて」のような問いかけを検出したとき、登録済みのプロジェクト一覧を additionalContext で Claude に渡し、AskUserQuestion ツールで選択メニューを表示させます。プロジェクト名を忘れたときや、他の人に「どんなプロジェクトがあるか」を見せたいときに便利です。
ポイント4:何もマッチしないときは何も返さない
result = find_repo(prompt)
if result:
...
print(json.dumps(output))
# マッチしない場合は何も出力しない
プロジェクト名が含まれないプロンプトのときは何も出力しません。Claude への余分なコンテキスト注入を避けるためです。毎回何かを送ると、無関係な会話でもリポジトリ情報が混入してノイズになります。
スクリプトの設置と登録
1. スクリプトを配置
cp repo_switcher.py ~/.claude/repo_switcher.py
chmod +x ~/.claude/repo_switcher.py
2. settings.json に登録
~/.claude/settings.json を編集します。hooks セクションがなければ追加します。
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "python3 /Users/yourname/.claude/repo_switcher.py"
}
]
}
]
}
}
パスは絶対パスで書いてください。~ は展開されないことがあります。
3. 動作確認
Claude Code を起動して、登録したプロジェクト名を含むメッセージを送ってみます。レスポンスの冒頭にシステム情報として [repo-switcher] から始まる行が見えれば動いています。
デバッグしたいときは、スクリプトを直接実行して確認できます。
echo '{"prompt": "受発注システムのバグを確認して"}' | python3 ~/.claude/repo_switcher.py
期待する出力:
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "[repo-switcher] プロンプトに「受発注システム」が含まれています。対応するリポジトリパスは: /path/to/order-system\n..."
}
}
なぜスキルではなくフックか
Claude Code には「スキル」という仕組みもあります。スキルは /my-skill のように明示的に呼び出すものです。フックは「送信するたびに自動で動く」ものです。
このスクリプトの価値は「毎回意識しなくても動く」点にあります。/repo-switch と毎回打つのでは、パスを手打ちするのと手間が変わりません。プロンプトに自然に出てくるプロジェクト名を拾って動くからこそ、摩擦がなくなります。「何かを自動化したい」ではなく「何かを意識させたくない」場合はフックが適しています。
プロジェクトを切り替えたら /clear を忘れずに
これは重要なことですが、repo_switcher.py はプロンプトごとに毎回パスを注入するので、Claude は新しいリポジトリを認識します。ただし前の会話で読み込んだファイルの中身やコードはコンテキストに残ったままです。
そのまま本格的な作業を始めると、Claude が「さっきのあのファイル」を参照してしまったり、別プロジェクトのコードと混同した回答を返してきたりと、コンテキストがカオスになります。
運用の目安はこうです。
| 作業の重さ | 対応 |
|---|---|
| ちょっと確認したい程度 | そのままでOK |
| 本格的に別プロジェクトの作業に入る |
/clear してからプロンプトを送る |
/clear はフックや settings.json の設定には影響しないので、実行後も repo_switcher.py はそのまま動き続けます。移動してから/clearするとclaudeを起動したディレクトリに戻りますので、先に/clearすることをお勧めします。
まとめ
| 要素 | 役割 |
|---|---|
UserPromptSubmit フック |
プロンプト送信のたびに自動実行 |
additionalContext |
Claude に追加情報を差し込む |
| キーワードの長い順マッチ | 短いキーワードによる誤検知を防ぐ |
| 日本語・英語の複数キー | 通称でも正式名でも引っかかる |
| マッチしないとき無出力 | 無関係な会話へのノイズ注入を防ぐ |
設定は REPOS 辞書にキーワードとパスを追加するだけなので、プロジェクトが増えても管理しやすい構造です。Claude Code を複数プロジェクトで使っている方はぜひ試してみてください。