/hook-scaffold スキル
Goal
新規フックの追加を、スクリプト雛形の生成から配線・検証まで、
本リポジトリの規約どおりに一貫した手順で完了させる。
Workflow
Step 1: 引数パース
$ARGUMENTS を以下のように解釈する:
- 第1引数: 生成するフック名(以降
HOOK_NAME。ファイル名は HOOK_NAME.sh)
- 第2引数以降: そのフックの用途の一言(自由記述、省略可)
引数なしの場合は対話でフック名と用途を尋ねる。HOOK_NAME は既存に倣い lowercase + ハイフンにする。
Step 2: 衝突チェックと既存重複の確認
ai-agents/hooks/HOOK_NAME.sh と ai-agents/settings/*/hooks/HOOK_NAME.sh の いずれにも存在しないこと を確認する。存在する場合は作成せず中断し、既存スクリプトの修正を案内する。
あわせて既存フックを読み、同種の処理(formatter / lint / guard / notify)が既にないか確認する。
formatter 系は 1 ファイル種別 1 スクリプトで揃っているので、対象拡張子が既存とかぶる場合は新規追加ではなく既存への追記を提案する。
Step 3: イベント選定とブロッキング可否の確認
「何をきっかけに走らせたいか」からイベントを選ぶ。現在このリポジトリが配線しているのは以下だけで、他イベントを使う場合は公式ドキュメント(https://code.claude.com/docs/en/hooks)で最新のスキーマを確認する。
| 用途 | claude のイベント | 現行の配線 | 配線ファイル |
|---|
| 起動時の環境チェック | SessionStart | toolchain-doctor.sh | settings.json |
| コンパクション前後の退避 | PreCompact / SessionStart(matcher compact) | context-anchor-save / -restore.sh | hooks.json |
| 毎プロンプトの文脈注入 | UserPromptSubmit | git-state.sh | hooks.json |
| Bash 実行前の遮断 | PreToolUse(matcher Bash) | guard-dangerous-bash.sh | hooks.json |
| ファイル編集後の整形 | PostToolUse(matcher Write|Edit|MultiEdit) | formatter 6 本 | hooks.json |
| 応答終了時のまとめ処理 | Stop | lint-changed.sh ほか | hooks.json |
| 通知 | Notification | notify-macos.sh | settings.json |
| worktree 作成時 | WorktreeCreate | worktree-create.sh | settings.json |
選定時に必ず確認すること:
- そのイベントは exit 2 でブロックできるか。
PreToolUse はブロックできるが PostToolUse はできない。ブロック不可のイベントに強制ロジックを載せると「黙って効かないフック」になる。
- 終了コードの意味:
0 = 正常(stdout が JSON なら構造化出力として解釈される)、2 = ブロッキング(stderr がメッセージ)、それ以外 = 非ブロッキングエラー。
hookSpecificOutput の形はイベントごとに違う。使う場合は転記済みの記憶に頼らず、必ず上記ドキュメントで確認する。
report-only(報告のみで止めない)で足りるなら、exit 0 + stderr への出力に留めるのが既定。
Step 3.5: 置き場所の決定
.claude/rules/hook-authoring.md の判断軸に従い、claude 側の置き場所を決める(以降 HOOK_ROOT)。判断が割れる場合はユーザーに確認する。
| ルート | 選ぶ条件 | 配線先 |
|---|
ai-agents/hooks | このマシン・このクローンに依存しない(既定) | ai-agents/hooks/hooks.json |
ai-agents/settings/claude/hooks | macOS GUI・ローカル FS レイアウト・ローカル mise のツールチェーンに依存する | settings/claude/settings.json |
サイドカー(設定ファイル等)を読むなら $HOME の絶対パスではなく SCRIPT_DIR 基準にする。plugin として動くときに $HOME/.claude/hooks/ は存在しない。
Step 4: 配線先の確認
既定は claude のみ。codex へは次の基準で展開する。cursor / copilot の設定は廃止済み(ADR-0009)。
- 展開する:
PreToolUse(matcher Bash)の遮断フック。入力が tool_input.command、遮断が exit 2 + stderr で claude と同じ契約(既存は guard-dangerous-bash)。
- 展開しない:
Stop / SessionStart / WorktreeCreate / macOS 通知など Claude 固有機構に依存するもの。
- 上記以外は、Codex 側のイベントと入力形式を公式ドキュメント(https://developers.openai.com/codex/hooks)で確認してから決める。
codex 用は ai-agents/settings/codex/hooks/ に claude 版とは別の自前コピーを置く。判定部が同じでも共有しない(claude 側の仕様変更に引きずられないため)。
Step 5: スクリプト生成
HOOK_ROOT/HOOK_NAME.sh を既存フック準拠で作る。定型は次のとおり
(インデントは既存フックに合わせてタブにする。以下は markdownlint の hard-tab 検査を避けるためスペース表記にしてあるので、書き出し後に shfmt -w を通して揃える):
#!/bin/bash
# mise-managed tools (non-interactive contexts do not run mise activate)
export PATH="${MISE_DATA_DIR:-$HOME/.local/share/mise}/shims:$PATH"
# stdin から JSON を読み込む
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | python3 -c "
import sys, json
data = json.load(sys.stdin)
print(data.get('tool_input', {}).get('file_path', ''))
")
# 対象外なら何もせず抜ける
if [[ "$FILE_PATH" != *.EXT ]]; then
exit 0
fi
if ! COMMAND "$FILE_PATH" 2>&1; then
echo "[HOOK_NAME] fail: $FILE_PATH" >&2
exit 1
fi
Step 6: 配線
JSON は既存エントリの末尾に追記する形にし、並び順の慣習(formatter は既存の並びに続ける)に従う。
- claude(
HOOK_ROOT が ai-agents/hooks): ai-agents/hooks/hooks.json の hooks.<Event> に
{"type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/HOOK_NAME.sh\""} を追記する。
PostToolUse なら既存の matcher: "Write|Edit|MultiEdit" グループの hooks 配列に足す(新しいグループを作らない)。
- claude(
HOOK_ROOT が ai-agents/settings/claude/hooks): ai-agents/settings/claude/settings.json の
hooks.<Event> に {"type": "command", "command": "~/.claude/hooks/HOOK_NAME.sh"} を追記する。
- codex:
ai-agents/settings/codex/hooks.json の hooks.PreToolUse(matcher: "^Bash$")に
{"type": "command", "command": "bash \"$HOME/.codex/hooks/HOOK_NAME.sh\"", "timeout": 10} を追記する
(timeout は秒)。
編集後、触った JSON すべてを jq . <file> にかけてパースできることを必ず確認する。
Step 7: 配布経路の確認
mise.toml の claude-settings-copy / codex-settings-copy は
ai-agents/scripts/copy-entries.sh でディレクトリ単位に配るため、
既存ディレクトリにファイルを 1 本足すだけなら mise / copy-entries.sh の変更は不要。
新しいディレクトリ階層を導入する場合のみタスク追加を検討する。この前提自体を毎回確認する。
ai-agents/hooks/ に置いたフックは plugin(ai-agents@my-pde)と claude-settings-copy の
両方から配られ、ローカルでは二重に発火する。移行期の既定の挙動なので、そのつもりで
report-only か冪等かを設計する。
Step 8: 検証
以下を実行し、エラーを潰してから完了とする。
chmod +x されていること(既存フックに合わせる)
shfmt -d <script> と shellcheck <script>(mise run lint:shell で一括でも可)
jq . で配線先 JSON がパースできること
- サンプル入力での単体実行:
echo '{"tool_input":{"file_path":"/tmp/x.sh"}}' | ./HOOK_NAME.sh
- デプロイは自動実行せず、
mise run settings-copy で配布される旨を案内する(codex に配線した場合は、配布後に Codex の /hooks で信頼レビューが要る旨も添える)
- 発火確認は
claude --debug 等で行う旨を案内する
Notes
- 既存スクリプトの上書き防止が最重要。Step 2 の非存在確認を必ず先に行い、衝突時は作成せず中断する。
- イベント一覧や JSON スキーマを SKILL.md に転記しない。Claude Code のフック仕様は変化が速いため、
本文には「このリポジトリ固有の配線規約」だけを書き、詳細は https://code.claude.com/docs/en/hooks を参照させる。
- 既定は claude 専用。codex への展開は Step 4 の基準に限る。
- Step 6 の JSON 編集を誤ると設定全体が読めなくなるため、
jq での妥当性確認を飛ばさない。
- 誤爆でフックが生成されるのを防ぐため
disable-model-invocation: true を付けている。明示呼び出し専用。
- 本スキルは「新規フックの立ち上げ」専任。既存フック定義の静的検査は
agents-lint 系の役割で、そちらには踏み込まない。