Superpowers の入口 — SessionStart Hook がエージェントを支配する仕組み¶
この記事で解説するプロジェクト
Superpowers — Claude Codeの動作をSkillで制御するフレームワーク
GitHub: obra/superpowers
なぜHookが必要なのか¶
前回の記事で、Superpowersは14個のSkillで構成されていると書いた。
しかし、ここに根本的な問題がある。Claude Codeはセッション開始時にSkillの名前とdescriptionしか読まない。Skill本文は、エージェントが「このSkillを使おう」と判断して初めて読み込まれる。
つまり、エージェントが自発的にSkillを使う気にならなければ、Superpowersの14個のSkillは存在しないのと同じだ。
エージェントのデフォルトの行動は「直接コードを書く」だ。Skill一覧を見て「あ、brainstormingを先にやるべきだな」と自分で判断することは、ほとんどない。
この問題を解決するのがSessionStart Hookだ。
全体の実行チェーン¶
Superpowersをインストールすると、以下のチェーンが登録される。
セッション開始(startup / clear / compact)
→ Claude Codeが hooks.json を読む
→ run-hook.cmd を実行
→ session-start スクリプトを実行
→ using-superpowers/SKILL.md の全文を読み込む
→ JSON形式で additionalContext として出力
→ エージェントのコンテキストに注入される
一つずつ見ていく。
ステップ1: hooks.json — トリガーの登録¶
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|clear|compact",
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start"
}
]
}
]
}
}
ポイントはmatcherの値だ。
| イベント | 発火タイミング |
|---|---|
startup | 新しいセッションを開始した時 |
clear | /clear コマンドを実行した時 |
compact | コンテキストが圧縮された時 |
compactが含まれている理由は重要だ。長い会話でコンテキストが圧縮されると、最初に注入されたSkill内容が消える可能性がある。compactでも再注入することで、会話がどれだけ長くなってもusing-superpowersが消えないことを保証している。
ステップ2: run-hook.cmd — クロスプラットフォーム対応¶
: << 'CMDBLOCK'
@echo off
REM Windows: cmd.exeがバッチ部分を実行し、bashを探して呼び出す
if exist "C:\Program Files\Git\bin\bash.exe" (
"C:\Program Files\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5
exit /b %ERRORLEVEL%
)
REM ...Git Bash, MSYS2, Cygwin を順番に探す
CMDBLOCK
# Unix: スクリプトを直接実行
exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
このファイルはcmd/bashのポリグロットスクリプトだ。
- Windows:
:はcmd.exeでは無視される。@echo off以降のバッチ部分が実行され、Git BashなどのBash環境を探してsession-startを呼び出す - Unix:
:はbashのno-op(何もしない)コマンド。ヒアドキュメントでバッチ部分をスキップし、最後のexec bashで直接実行する
Bashが見つからない場合はサイレントに終了する(exit 0)。プラグインの他の機能は動作し続け、SessionStartのコンテキスト注入だけがスキップされる。
ステップ3: session-start — 本体¶
ここが核心だ。このスクリプトは3つのことをする。
3-1. SKILL.mdの全文を読み込む¶
using-superpowers/SKILL.mdはフロントマター含めて約120行。この全文がコンテキストに注入される。
3-2. JSONに安全にエスケープする¶
escape_for_json() {
local s="$1"
s="${s//\\/\\\\}" # バックスラッシュ
s="${s//\"/\\\"}" # ダブルクオート
s="${s//$'\n'/\\n}" # 改行
s="${s//$'\r'/\\r}" # キャリッジリターン
s="${s//$'\t'/\\t}" # タブ
printf '%s' "$s"
}
Skill内容をJSON文字列として埋め込むため、特殊文字をエスケープする。bashのパラメータ置換(${s//old/new})を使っており、文字単位のループより高速だ。
3-3. プラットフォームに応じた出力¶
if [ -n "${CURSOR_PLUGIN_ROOT:-}" ]; then
# Cursor
printf '{"additional_context": "%s"}\n' "$session_context"
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -z "${COPILOT_CLI:-}" ]; then
# Claude Code
printf '{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "%s"}}\n' "$session_context"
else
# Copilot CLI / その他
printf '{"additionalContext": "%s"}\n' "$session_context"
fi
各プラットフォームが期待するJSONフォーマットが異なる。
| プラットフォーム | 判定方法 | JSONフォーマット |
|---|---|---|
| Cursor | CURSOR_PLUGIN_ROOTが存在 | additional_context(スネークケース) |
| Claude Code | CLAUDE_PLUGIN_ROOTが存在 + COPILOT_CLIなし | hookSpecificOutput.additionalContext(ネスト) |
| Copilot CLI | COPILOT_CLIが存在 | additionalContext(トップレベル) |
| その他 | デフォルト | additionalContext(SDK標準) |
重複注入の防止
Claude Codeはadditional_contextとhookSpecificOutputの両方を読むが、重複排除はしない。そのため、Claude Codeの場合はネスト形式のみを出力し、トップレベルのadditional_contextは出力しない。両方出力すると、同じ内容が2回注入されてしまう。
注入される内容¶
最終的にエージェントのコンテキストに注入されるのは、以下のような構造だ。
<EXTREMELY_IMPORTANT>
You have superpowers.
**Below is the full content of your 'superpowers:using-superpowers' skill
- your introduction to using skills. For all other skills, use the 'Skill' tool:**
---
name: using-superpowers
description: Use when starting any conversation...
---
<EXTREMELY-IMPORTANT>
If you think there is even a 1% chance a skill might apply...
</EXTREMELY-IMPORTANT>
## The Rule
Invoke relevant or requested skills BEFORE any response or action.
## Red Flags
| Thought | Reality |
...
</EXTREMELY_IMPORTANT>
注目すべき点:
<EXTREMELY_IMPORTANT>タグで全体を囲む — Claude Codeのシステムプロンプトにおいて、このタグは最高優先度のコンテンツであることを示す- Skill全文を注入する — descriptionだけでなく、Red Flagsテーブル、フローチャート、優先度ルールのすべてが含まれる
- 他のSkillは注入しない —
using-superpowersだけが特別扱い。他の13個のSkillは、エージェントが必要に応じてSkillツールで読み込む
レガシーSkillディレクトリの検出¶
session-startスクリプトにはもう一つの役割がある。
legacy_skills_dir="${HOME}/.config/superpowers/skills"
if [ -d "$legacy_skills_dir" ]; then
warning_message="⚠️ WARNING: Superpowers now uses Claude Code's skills system.
Custom skills in ~/.config/superpowers/skills will not be read.
Move custom skills to ~/.claude/skills instead."
fi
旧バージョンのSuperpowersは~/.config/superpowers/skillsにカスタムSkillを配置していた。現在はClaude Code標準の~/.claude/skillsに移行している。旧ディレクトリが残っている場合、最初の応答で必ずユーザーに警告するよう指示が注入される。
なぜSkillの自動スキャンだけでは不十分なのか¶
Claude Codeは起動時にすべてのプラグインのSkillを自動スキャンし、名前とdescriptionをシステムプロンプトに追加する。
The following skills are available for use with the Skill tool:
- superpowers:brainstorming
- superpowers:test-driven-development
- superpowers:writing-plans
...
これだけで十分に見えるが、2つの問題がある。
問題1: エージェントは能動的にSkillを使わない¶
Skillの存在を知っていることと、実際に使うことは全く違う。
会社の新入社員が「社内マニュアルがあります」と聞いても読まないのと同じだ。エージェントは「機能を追加して」と言われれば、Skill一覧を確認する前にコードを書き始める。
問題2: descriptionだけでは行動を変えられない¶
descriptionはSkillの「いつ使うか」を記述するものだ。「Skillを使う前にまずSkillを確認しろ」というメタレベルの指示は、descriptionの範囲外にある。
using-superpowersはメタSkillだ。「他のSkillをいつ・どう使うか」を教えるSkillであり、description経由の自然な発見では機能しない。だからSessionStart Hookで全文を強制注入する必要がある。
Cursorでの動作¶
Cursorは別のHooks設定ファイルを使う。
Claude Codeのhooks.jsonとの違い:
| Claude Code | Cursor | |
|---|---|---|
| matcher | "startup\|clear\|compact" | なし(セッション開始時のみ) |
| command | run-hook.cmd経由 | session-startを直接実行 |
| JSON出力 | hookSpecificOutput(ネスト) | additional_context(スネークケース) |
session-startスクリプト自体は共通で、環境変数(CURSOR_PLUGIN_ROOT / CLAUDE_PLUGIN_ROOT)で出力フォーマットを切り替える。
まとめ¶
| 要素 | 役割 |
|---|---|
hooks.json | SessionStartイベントにHookを登録する |
run-hook.cmd | Windows/Unix両対応のポリグロットラッパー |
session-start | SKILL.md全文を読み、プラットフォーム別JSONで出力 |
using-superpowers | エージェントに「Skillを使え」と強制するメタSkill |
compact対応 | コンテキスト圧縮後も再注入し、消失を防ぐ |
Hookが1つしか登録されていないのに、Superpowersの全Skillが機能する。using-superpowersが注入されれば、そこから先はエージェント自身が他のSkillを発見し、呼び出すからだ。
1つのHookで14個のSkillを支配する。これがSuperpowersの入口設計だ。
次回は、各Skillの中身である行動制御の設計哲学を詳しく解説する。