Hooks を使うと、Qoder IDE や JetBrains プラグインの実行フローにおける重要なポイントで、コードを変更することなくカスタムロジックを挿入できます。JSON 設定ファイルを編集するだけで、たとえば次のようなことを実現できます。
- ツール実行前に危険な操作をブロック
- ファイル書き込みのたびに自動で lint を実行し、コードスタイルを統一
- Agent のタスク完了時にデスクトップ通知を送り、画面に張り付く必要をなくす
Prompt による指示とは異なり、Hooks は確定的に動作します。イベントが発火すればスクリプトは必ず実行され、モデルの解釈に結果が左右されることはありません。
Hooks の機能は入口ごとに異なります。本ページは Qoder IDE / JetBrains プラグイン(12 イベント、command と http の 2 タイプのみ)に対応します。Qoder CLI Hooks と QoderWork Hooks は別ページで説明しています。IDE と CLI は同じ設定ファイルを共有しますが、各入口は自身がサポートするイベントのみを実行します。
サポートされるイベント
IDE / JB プラグインが現在サポートしている Hook イベントは以下の 12 種類です。
| イベント名 | 発火タイミング | ブロック可能 |
|---|
| SessionStart | セッションの開始時または再開時 | いいえ |
| UserPromptSubmit | ユーザーが Prompt を送信した後、Agent が処理を開始する前 | はい |
| PreToolUse | ツール実行の前 | はい |
| PermissionRequest | ツールにユーザーの承認が必要になった時 | はい |
| PostToolUse | ツール実行の成功後 | いいえ |
| PostToolUseFailure | ツール実行の失敗後 | いいえ |
| SubagentStart | サブエージェントの開始時 | いいえ |
| SubagentStop | サブエージェントの停止時 | いいえ |
| Stop | Agent がレスポンスを返し終えた時 | はい |
| SessionEnd | セッションの終了時 | いいえ |
| PreCompact | コンテキスト圧縮の前 | いいえ |
| Notification | ユーザー向け通知が発行された時 | いいえ |
クイックスタート
以下の例では、rm -rf のような危険なコマンドをブロックする方法を紹介します。
スクリプトを作成
mkdir -p ~/.qoder/hooks
cat > ~/.qoder/hooks/block-rm.sh << 'EOF'
#!/bin/bash
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command')
if echo "$command" | grep -q 'rm -rf'; then
echo "Dangerous command blocked: $command" >&2
exit 2
fi
exit 0
EOF
chmod +x ~/.qoder/hooks/block-rm.sh
設定を追加
~/.qoder/settings.json に以下を追加します。{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.qoder/hooks/block-rm.sh"
}
]
}
]
}
}
動作を確認
IDE を開き、Qoder プラグインパネルで rm -rf を含むコマンドの実行を Agent に指示してください。Hook が実行をブロックし、エラーメッセージを Agent にフィードバックします。
ユースケース
| シナリオ | 対応イベント | 説明 |
|---|
| 危険コマンドの遮断 | PreToolUse | rm -rf や DROP TABLE などの実行前にブロック |
| ファイルパスの検証 | PreToolUse | 指定ディレクトリ内でのみファイルの作成・編集を許可 |
| 自動 Lint / フォーマット | PostToolUse | ファイル書き込みのたびに ESLint / Prettier を自動実行 |
| 監査ログ | PostToolUse | すべてのツール呼び出しを記録し、セキュリティ監査に活用 |
| 失敗時のモニタリング | PostToolUseFailure | ツール実行が失敗した際にアラートを送信、またはエラーログを記録 |
| Prompt の内容チェック | UserPromptSubmit | ユーザー入力に機密情報(パスワード、キーなど)が含まれていないか検出 |
| コンテキストの自動注入 | UserPromptSubmit | Prompt の末尾にプロジェクト規約やコーディング規約を自動付加 |
| デスクトップ通知 | Stop | Agent 完了時にシステム通知を表示 |
仕組み
Hook の利用は、スクリプトを書く → 設定に登録する → 自動で有効になる、という 3 ステップです。
Agent がライフサイクル上の特定のポイント(例: ツール呼び出し前)に到達すると、プラグインは該当するイベントに Hook が登録されているかを確認します。
- プラグインは起動時にすべての Hook 設定を読み込む
- Agent の実行中にイベントポイント(例:
PreToolUse)に到達する
- プラグインはそのイベントに登録された Hook グループを順に走査し、
matcher で現在のコンテキストと照合する
- マッチした Hook は登録順にシェルスクリプトを実行する
- スクリプトは
stdin からイベントコンテキスト(JSON)を受け取り、exit code と stdout で結果を返す
- プラグインはその結果に基づいて後続の動作(続行またはブロック)を決定する
前提条件
- jq: サンプルスクリプトでは JSON の解析に jq を使用しています。macOS では
brew install jq、Linux では apt install jq でインストールしてください。
- スクリプト権限: すべての Hook スクリプトに実行権限が必要です(
chmod +x)。
Hooks の作成
1. 要件の整理: イベントとマッチ範囲を決める
まず、どのタイミングで介入し、どの操作を対象にするかを明確にします。
「いつ」介入するか? 「どの操作」を対象にするか?
↓ ↓
イベントを選択 matcher を設定
| 要件 | イベント | matcher |
|---|
| Shell コマンド実行前にチェックしたい | PreToolUse | "Bash" |
| ファイルの書き込み・編集後に処理したい | PostToolUse | "Write|Edit" |
| ツール実行の失敗を記録したい | PostToolUseFailure | "Bash" または未指定 |
| すべての Prompt を審査したい | UserPromptSubmit | 未指定(すべてにマッチ) |
| Agent の停止時に通知したい | Stop | 未指定(すべてにマッチ) |
| MCP ツールのみ対象にしたい | PreToolUse | "mcp__.*" |
2. Hook スクリプトを書く
Hook スクリプトは標準的なシェルスクリプトで、以下の入出力規約に従います。
入力: stdin から JSON 形式のイベントコンテキストを受け取る
出力: exit code で動作を制御する
exit 0 → 続行(操作を許可)
exit 2 → ブロック(操作を停止し、stderr の内容を会話に注入)
それ以外 → エラー(操作は続行し、stderr をユーザーに表示)
スクリプトテンプレート:
#!/bin/bash
# 1. Read JSON input from stdin
input=$(cat)
# 2. Extract fields with jq
# Each event has different fields — see the "Hooks イベント" section
tool_name=$(echo "$input" | jq -r '.tool_name')
tool_input=$(echo "$input" | jq -r '.tool_input')
# 3. Implement your decision logic
if [ "$tool_name" = "Bash" ]; then
command=$(echo "$input" | jq -r '.tool_input.command')
# Check for dangerous operations
if echo "$command" | grep -qE 'rm\s+-rf|DROP\s+TABLE'; then
# Block: exit 2 + stderr message is fed back to Agent
echo "Operation denied: $command" >&2
exit 2
fi
fi
# 4. Allow
exit 0
exit code に加えて、exit 0 の際に JSON を出力することで、より細かい制御が可能です。#!/bin/bash
input=$(cat)
# Output JSON for fine-grained control (only parsed when exit 0)
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"This operation is not allowed"}}'
exit 0
3. 設定ファイルへの登録
スクリプトのパスを、設定ファイルの該当イベントに記述します。
{
"hooks": {
"EventName": [
{
"matcher": "MatchPattern (optional)",
"hooks": [
{
"type": "command",
"command": "path/to/script"
}
]
}
]
}
}
4. テストとデバッグ
ターミナルでパイプを使って直接テストできます。
# Simulate a PreToolUse event
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"},"hook_event_name":"PreToolUse"}' \
| ~/.qoder/hooks/block-rm.sh
echo "Exit code: $?"
stderr 出力(ブロックメッセージ)を確認するには:
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' \
| ~/.qoder/hooks/block-rm.sh 2>&1
Hooks の設定
設定ファイルの場所
Hook 設定は以下のファイルから読み込まれます。複数レベルの設定はマージされて実行されます(優先度の低い順)。
| パス | スコープ | 優先度 | 共有可能 | 説明 |
|---|
~/.qoder/settings.json | ユーザーレベル | 1(最低) | いいえ | ユーザー個人の設定。すべてのプロジェクトに適用 |
.qoder/settings.json | プロジェクトレベル | 2 | はい | Git にコミットしてチームで共有可能 |
.qoder/settings.local.json | プロジェクトレベル(ローカル) | 3 | いいえ | gitignore 対象。個人の開発用設定 |
IDE / JB プラグインと CLI は同じ設定ファイルを共有します。現在のバージョンではホットリロードに未対応のため、設定変更後は IDE の再起動が必要です。
設定フォーマット
{
"hooks": {
"EventName": [
{
"matcher": "MatchPattern",
"hooks": [
{
"type": "command",
"command": "command to execute"
}
]
}
]
}
}
| フィールド | 必須 | 説明 |
|---|
type | はい | "command" または "http" |
command | はい | 実行するシェルコマンドまたはスクリプトのパス |
timeout | いいえ | タイムアウト(秒)。デフォルトは 30 秒 |
matcher | いいえ | マッチ条件。未指定の場合はそのイベントのすべてのトリガーにマッチ |
if | いいえ | より細かい Hook 単位のフィルタ条件。"ToolName" や "ToolName(arg_pattern)" の形式 |
async | いいえ | true の場合、Hook は現在の操作をブロックせずにバックグラウンドで実行される |
asyncRewake | いいえ | true の場合、バックグラウンドで実行され、結果でモデルを起こせる——時間のかかるチェックに適している |
statusMessage | いいえ | Hook の実行中にステータスバーに表示されるカスタムの説明 |
1 つのイベントに対して複数の matcher グループを設定でき、各グループには複数の Hook コマンドを含められます。
matcher マッチルール
matcher は Hook の発火対象を絞り込むために使用します。イベントごとにマッチ対象のフィールドが異なります(各イベントの説明を参照)。
| 記法 | 意味 | 例 |
|---|
未指定または "*" | すべてにマッチ | すべてのツールでトリガー |
| 完全一致の値 | 完全一致 | "Bash" は Bash ツールの時のみトリガー |
| 区切り | 複数の値にマッチ | "Write|Edit" は Write または Edit の時にトリガー |
| 正規表現 | 正規表現マッチ | "mcp__.*" はすべての MCP ツールにマッチ |
ツール名マッピング
Qoder IDE はネイティブのツール名と Claude Code 互換のツール名の 2 セットをサポートしています。Hook の設定ではどちらでも使用でき、プラグイン内部で統一的にマッピングしてからマッチングを行います。たとえば matcher: "Bash" と matcher: "run_in_terminal" は同じ意味です。
| Qoder IDE ネイティブ名 | 互換名 | 説明 |
|---|
run_in_terminal | Bash | シェルコマンドの実行 |
read_file | Read | ファイル内容の読み取り |
create_file | Write | ファイルの作成・書き込み |
search_replace | Edit | ファイルの編集 |
edit_file | - | diff / コードブロックによる編集 |
get_terminal_output | - | バックグラウンド端末の出力取得 |
delete_file | - | ファイルの削除 |
grep_code | Grep | ファイル内容の検索 |
search_file | Glob | ファイル名のマッチング |
list_dir | LS | ディレクトリの一覧表示 |
Agent | Task | サブエージェントの起動(旧称 task) |
Skill | - | スキルの呼び出し |
search_web | WebSearch | Web 検索 |
fetch_content | WebFetch | Web コンテンツの取得 |
todo_write | TodoWrite | TODO の書き込み |
ask_user_question | - | ユーザーへの質問 |
search_memory | - | メモリの検索 |
update_memory | - | メモリの更新 |
switch_mode | - | モードの切り替え |
create_plan | - | プランの作成 |
run_preview | - | Web アプリのプレビュー |
get_problems | - | IDE の診断情報の取得 |
fetch_rules | - | ルールの読み込み |
ImageGen | - | 画像の生成 |
mcp__<server>__<tool> | 同左 | MCP ツール |
Hook スクリプトの書き方
Hook スクリプトは stdin から JSON 入力を受け取り、exit code と stdout で動作を制御します。このセクションでは全イベント共通の入出力フォーマットを説明します。各イベント固有のフィールドについてはHooks イベントを参照してください。
Hook スクリプトは stdin から JSON データを受け取ります。すべてのイベントで共通するフィールドは以下のとおりです。
| フィールド | 説明 |
|---|
session_id | 現在のセッション ID |
cwd | 現在の作業ディレクトリ |
hook_event_name | トリガーされたイベント名 |
transcript_path | セッションコンテキスト JSON ファイルのパス |
request_set_id | 現在の IDE リクエストラウンドの ID |
tool_name | ツール名(ツール関連イベントのみ) |
tool_input | ツールの入力パラメータ |
tool_response | ツールの実行結果(PostToolUse のみ)。IDE では現在、通常の文字列として渡されます |
extra.email | ユーザーの Git メールアドレス |
extra.repo | リポジトリパス(group/repo 形式) |
extra.branch | 現在のブランチ |
extra.request_time | リクエスト時刻(RFC3339) |
extra.response_time | レスポンス時刻(RFC3339) |
extra.full_diff_text | 変更内容の完全な diff(編集系ツールの PostToolUse のみ) |
イベントによっては、上記に加えて追加のフィールドが含まれます(各イベントの説明を参照)。すべてのフィールドは省略可能なものとして扱ってください。フィールドが定義されていても、実行パスによっては値が設定されない場合があります。
jq で入力を解析する例:
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')
Hook は exit code と stdout で動作を制御します。
exit code による基本動作:
| Exit Code | 意味 | 動作 |
|---|
0 | 成功 | 操作を続行し、stdout の JSON 解析を試みる |
2 | ブロック | 操作を停止し、stderr の内容を会話に注入(ブロック可能なイベントのみ有効) |
| その他 | エラー | 操作は続行するが、stderr をユーザーに表示 |
stdout JSON を使うと、一部のイベントでより細かい制御が可能です。サポートされるフィールドの詳細は各イベントの説明を参照してください。IDE は終了コード 0 と 2 の両方で stdout の JSON 解析を試みます。exit 2 の stdout がプレーンテキストとして扱われると仮定しないでください。
共通の stdout フィールド
各イベント固有の hookSpecificOutput 内のフィールドに加えて、以下のトップレベルのフィールドはすべてのイベントで使用できます:
| フィールド | 説明 |
|---|
systemMessage | ユーザーに表示されるメッセージ |
continueWithPrompt | Agent が実行を続行するかどうか |
decision | "block" で汎用的なブロックを決定。権限の決定には代わりに hookSpecificOutput.permissionDecision を使用 |
reason | 決定の理由(一部のイベントは独自の reason フィールドを定義) |
updatedToolOutput | ツールの出力を置き換える |
hookSpecificOutput | イベント固有フィールドのコンテナ(各イベントの説明を参照) |
環境変数
Hook スクリプトの実行時に、プラグインは以下の環境変数を注入します。スクリプトから参照できます:
| 変数 | 説明 |
|---|
QODER_SESSION_ID | セッション ID |
QODER_TOOL_NAME | 現在のツール名 |
QODER_CWD | 作業ディレクトリ |
QODER_TRANSCRIPT_PATH | Transcript ファイルのパス |
QODER_TOOL_INPUT_FILE_PATH | ツールが操作するファイルパス(該当する場合) |
Hooks イベント
SessionStart
セッションの開始時または再開時に発火します。セッションの開始時に起動コンテキスト(プロジェクト規約や環境情報など)を注入する用途に適しています。
matcher マッチ: マッチ対象のフィールドはありません。
追加入力フィールド:
{
"session_id": "abc-123",
"cwd": "/path/to/project",
"hook_event_name": "SessionStart",
"type": "startup",
"model": "Auto"
}
| フィールド | 型 | 説明 |
|---|
type | string | セッション開始の種別。IDE は現在 "startup" を渡す |
model | string | セッションのモデル設定 |
stdout JSON 出力フィールド(exit 0 の場合):
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "## Environment\n..."
}
}
UserPromptSubmit
ユーザーが IDE プラグインパネルで Prompt を送信した後、Agent が処理を開始する前に発火します。Prompt の審査やコンテンツフィルタリング、コンテキストの自動補完などに利用できます。
matcher マッチ: マッチ対象のフィールドはなく、すべてのユーザー入力に対して一律に発火します。
追加入力フィールド:
{
"session_id": "abc-123",
"cwd": "/path/to/project",
"hook_event_name": "UserPromptSubmit",
"prompt": "Write a sort function for me"
}
Prompt 送信のブロック: exit code 2 を返すと、stderr の内容がエラーメッセージとしてユーザーに表示され、Agent はその Prompt を処理しません。
stdout JSON 出力フィールド(exit 0 の場合):
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "## Current Git status\n..."
}
}
| フィールド | 型 | 説明 |
|---|
hookSpecificOutput.hookEventName | string | "UserPromptSubmit" 固定 |
hookSpecificOutput.additionalContext | string | 追加コンテキスト情報。Agent の会話に注入される |
例: Prompt にプロジェクト規約を自動付加
#!/bin/bash
input=$(cat)
prompt=$(echo "$input" | jq -r '.prompt')
# Automatically append coding standards reminder
echo '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"Please follow the coding standards in the project .editorconfig."}}'
exit 0
ツール実行前に発火します。ツールの実行をブロックできます。 最もよく使われるイベントで、危険コマンドの遮断やファイルパスの検証、権限チェックなどに適しています。
matcher マッチ: ツール名(Bash、Write、Edit、Read、Glob、Grep や、MCP ツール名 mcp__server__tool など)
追加入力フィールド:
{
"session_id": "abc-123",
"cwd": "/path/to/project",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "rm -rf /tmp/build" }
}
ツール実行のブロック: exit code 2 を返すと、stderr の内容がエラーとして Agent に返されます。完全な例はクイックスタートを参照してください。
stdout JSON 出力フィールド(exit 0 の場合):
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Safe read operation",
"updatedInput": { "command": "npm test --coverage" },
"additionalContext": "Added coverage flag"
}
}
| フィールド | 型 | 説明 |
|---|
hookSpecificOutput.hookEventName | string | "PreToolUse" 固定 |
hookSpecificOutput.permissionDecision | string | "allow"(許可)、"deny"(拒否)、"ask"(ユーザーに確認) |
hookSpecificOutput.permissionDecisionReason | string | 決定理由。deny や ask の場合に Agent またはユーザーに表示される |
hookSpecificOutput.updatedInput | object | 変更後のツール入力パラメータ(任意。ツールの呼び出し内容を書き換える場合に使用) |
hookSpecificOutput.additionalContext | string | 追加コンテキスト情報(任意) |
PermissionRequest
ツール呼び出しにユーザーの承認が必要になった時に発火します。承認可否を自動で決定できます。 安全な操作の自動許可や、ポリシー違反の自動拒否を、ユーザーへの確認なしで行う用途に適しています。
matcher マッチ: ツール名
追加入力フィールド:
{
"session_id": "abc-123",
"cwd": "/path/to/project",
"hook_event_name": "PermissionRequest",
"tool_name": "Bash",
"tool_input": { "command": "rm -rf node_modules" },
"tool_use_id": "call_01ABC123"
}
stdout JSON 出力フィールド(exit 0 の場合):
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"permissionDecision": "allow",
"permissionDecisionReason": "Safe cleanup command",
"updatedInput": { "command": "rm -rf node_modules" }
}
}
| フィールド | 型 | 説明 |
|---|
hookSpecificOutput.permissionDecision | string | "allow"(許可)、"deny"(拒否)、"ask"(ユーザーへの確認にフォールバック) |
hookSpecificOutput.permissionDecisionReason | string | 決定理由 |
hookSpecificOutput.updatedInput | object | 変更後のツール入力パラメータ(任意) |
このイベントの出力構造は IDE と CLI で異なります。CLI はネストされた decision.behavior オブジェクトを想定しています。出力を調整せずに同じスクリプトを両製品で使い回さないでください。
PostToolUse
ツール実行の成功後に発火します。ブロックはできません。自動 lint やログ記録、結果の分析などに適しています。
matcher マッチ: ツール名
追加入力フィールド:
{
"session_id": "abc-123",
"cwd": "/path/to/project",
"hook_event_name": "PostToolUse",
"tool_name": "Write",
"tool_input": { "file_path": "/path/to/file.ts", "file_content": "..." },
"tool_response": "File written successfully"
}
stdout JSON 出力フィールド(exit 0 の場合):
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"feedback": "File formatted with Prettier. 3 issues auto-fixed."
}
}
| フィールド | 型 | 説明 |
|---|
hookSpecificOutput.hookEventName | string | "PostToolUse" 固定 |
hookSpecificOutput.feedback | string | フィードバック情報。ユーザーに表示される(例: lint 結果のサマリー) |
PostToolUseFailure
ツール実行の失敗後に発火します。ブロックはできません。エラーの監視やリトライの提案、ログ記録などに適しています。
matcher マッチ: ツール名
追加入力フィールド:
{
"session_id": "abc-123",
"cwd": "/path/to/project",
"hook_event_name": "PostToolUseFailure",
"tool_name": "Bash",
"tool_input": { "command": "npm test" },
"tool_use_id": "call_01ABC123",
"error": "Command exited with non-zero status code 1",
"is_interrupt": false
}
| フィールド | 型 | 説明 |
|---|
error | string | ツール実行のエラーメッセージ |
is_interrupt | boolean | 失敗が中断によるものかどうか |
Stop
Agent がレスポンスを返し終えた後(実行待ちのツール呼び出しがない状態)に発火します。Agent の停止をブロックできます。品質ゲートやデスクトップ通知、ログ記録、タスクのステータス報告などに適しています。
matcher マッチ: マッチ対象のフィールドはなく、Agent の停止時に一律で発火します。
追加入力フィールド:
{
"session_id": "abc-123",
"cwd": "/path/to/project",
"hook_event_name": "Stop",
"stop_hook_active": true,
"last_assistant_message": "I have finished writing the sort function."
}
| フィールド | 型 | 説明 |
|---|
stop_hook_active | boolean | 直前の Stop Hook のブロックにより Agent がリトライ中の場合は true。スクリプトはこのフィールドを確認し、true の場合は exit 0 して無限ループを防ぐ必要があります |
last_assistant_message | string | Agent の最後のテキスト応答 |
Agent の停止をブロック: exit 2。ブロック理由はユーザーメッセージとして会話に注入され、Agent は作業を続行します。
stdout JSON 出力フィールド(exit 0 の場合):
{
"decision": "block",
"reason": "Tests failing. Fix them before completing."
}
| フィールド | 型 | 説明 |
|---|
decision | string | "block"(停止をブロックし、Agent に作業を継続させる) |
reason | string | ブロックの理由。メッセージとして会話に注入される |
無限ループの防止: Stop Hook が Agent をブロック(exit 2)すると、Agent はリトライして再び Stop イベントが発火し、その際 stop_hook_active: true になります。スクリプトは必ずこのフィールドを確認し、true の場合は exit 0 してください。さもないと無限にブロックし続けます。
SubagentStart
サブエージェントの開始時に発火します。ブロックはできません。サブエージェントの監査や、サブエージェントへのコンテキスト注入に適しています。
matcher マッチ: マッチ対象のフィールドはありません。
追加入力フィールド:
{
"session_id": "abc-123",
"cwd": "/path/to/project",
"hook_event_name": "SubagentStart",
"agent_id": "a1b2c3d4",
"agent_type": "task"
}
stdout JSON 出力フィールド(exit 0 の場合): hookSpecificOutput.additionalContext — サブエージェントの会話に注入されるコンテキスト。
SubagentStop
サブエージェントの停止時に発火します。IDE ではブロックできません。サブエージェントの実行結果の記録に適しています。
matcher マッチ: マッチ対象のフィールドはありません。
追加入力フィールド:
{
"session_id": "abc-123",
"cwd": "/path/to/project",
"hook_event_name": "SubagentStop",
"agent_id": "a1b2c3d4",
"agent_type": "task",
"stop_hook_active": false,
"agent_transcript_path": "/path/to/agent-transcript.jsonl",
"last_assistant_message": "Sub-task finished."
}
| フィールド | 型 | 説明 |
|---|
agent_id | string | サブエージェント ID |
agent_type | string | サブエージェントの種別 |
stop_hook_active | boolean | Hook のブロック後に停止がリトライされているかどうか |
agent_transcript_path | string | サブエージェントの transcript ファイルのパス |
last_assistant_message | string | サブエージェントの最後のテキスト応答 |
SessionEnd
セッションの終了時に発火します。ブロックはできません。クリーンアップやセッションのアーカイブなどの副作用処理に適しています。
matcher マッチ: マッチ対象のフィールドはありません。
追加入力フィールド:
{
"session_id": "abc-123",
"cwd": "/path/to/project",
"hook_event_name": "SessionEnd",
"reason": "exit"
}
終了理由は入力フィールドの reason から読み取ってください。(過去の matcher メタデータでは exit_reason という名前が使われていましたが、スクリプトは stdin の実際の reason フィールドを参照してください。)
PreCompact
コンテキスト圧縮の前に発火します。IDE ではこのイベントは通知と副作用処理のためのもので、圧縮をブロックすることはできません。
matcher マッチ: トリガー方法(manual はユーザーによる手動圧縮、auto はコンテキスト上限に近づいた際の自動圧縮)
追加入力フィールド:
{
"session_id": "abc-123",
"cwd": "/path/to/project",
"hook_event_name": "PreCompact",
"trigger": "auto",
"custom_instructions": ""
}
Notification
プラグインがユーザー向け通知を発行した時に発火します。ブロックはできません。通知を外部チャネル(デスクトップ、IM など)に転送する用途に適しています。
matcher マッチ: 通知タイプ
追加入力フィールド:
{
"session_id": "abc-123",
"cwd": "/path/to/project",
"hook_event_name": "Notification",
"notification_type": "permission_prompt",
"title": "Permission Required",
"message": "Agent is requesting permission to run: rm -rf node_modules"
}
stdout JSON 出力フィールド(exit 0 の場合): hookSpecificOutput.additionalContext。
注意事項
- タイムアウト: Hook スクリプトのデフォルトタイムアウトは 30 秒で、
timeout フィールドで Hook ごとに設定できます。タイムアウトするとスクリプトは強制終了され、続行(許可)として扱われます。
- エラー処理: スクリプトが異常終了した場合(exit code が 0 でも 2 でもない場合)、エラーメッセージがユーザーに表示されますが、Agent の処理は中断されず続行されます。
- スクリプト権限: スクリプトに実行権限が付与されていることを確認してください(
chmod +x)。
- 設定のマージ: 複数レベルの設定ファイルに同一イベントの Hook がある場合、優先度の低い順に実行されます。いずれかの Hook がブロック(exit 2)を返した時点で、以降の Hook は実行されません。
- jq の依存: サンプルスクリプトでは JSON 解析に
jq を使用しています。システムに jq がインストールされていることを確認してください(macOS: brew install jq、Linux: apt install jq)。
ベストプラクティスシナリオ
Hooks が向いているユーザー
Hooks の価値は役割によって異なります。以下は役割別の対応表です。
個人開発者向け
| シナリオ | 説明 | 実践 |
|---|
| Prompt の強化 | プロジェクト固有の Skill やコーディング規約を自動で注入し、毎回手入力する手間をなくす | シナリオ 1 |
| 機密情報の遮断 | パスワード、キー、内部 IP がモデルに送信されるのを防ぐ | シナリオ 2 |
| 危険コマンドの遮断 | rm -rf、git push --force などの破壊的なコマンドをブロック | シナリオ 6 |
| Harness の自己進化 | セッション終了時に再利用できる知見を自動検出し、振り返りをトリガー | シナリオ 8 |
チーム / 企業向け
| シナリオ | 説明 | 実践 |
|---|
| Rule / Skill の利用状況分析 | チーム全体でどの Rules や Skills が発火しているかを追跡し、資産の品質を評価 | シナリオ 3 |
| ファイル編集の追跡 | Agent が変更したファイルを記録し、変更監査や影響範囲の分析に活用 | シナリオ 4 |
| 全体的な利用状況分析 | 会話内容、ツール呼び出し、モデルの応答を収集し、効率分析のための全パイプラインを構築 | シナリオ 5 |
| 安全性の制御 | チーム全体で共通の危険コマンドブラックリストを強制 | シナリオ 6 |
| 品質ゲート | Agent の完了前にビルドやテストを自動実行し、失敗した場合はブロックして修正させる | シナリオ 7 |
個人向けのシナリオは通常 ~/.qoder/settings.json(ユーザーレベル)または .qoder/settings.local.json(プロジェクトレベルのローカル設定)に記述します。チーム向けのシナリオは .qoder/settings.json(プロジェクトレベル)に記述し、Git にコミットして全員に同じ設定が適用されるようにしてください。
シナリオ 1:Prompt の強化 — Skill の自動注入
課題: 毎回手動で Skill を指定する必要があり、プロジェクト固有のコンテキストの読み込みを忘れてしまう。
解決策: UserPromptSubmit Hook で Prompt にヒントを自動注入し、特定の Skill を使うよう Agent を誘導します。
設定:
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": ".qoder/hooks/inject-skill-hint.sh"
}
]
}
]
}
}
スクリプト .qoder/hooks/inject-skill-hint.sh:
#!/bin/sh
# 機能:Prompt の送信時に Skill 利用のヒントを注入する(セッションごとに 1 回のみ)
INPUT=$(cat)
# === セッション単位の重複排除:同一 session では 1 回のみ注入 ===
# 将来的には SessionStart イベント(近日対応予定)で代替でき、重複排除ロジックは不要になります
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty')
DEDUP_DIR="/tmp/hook-dedup"
mkdir -p "$DEDUP_DIR"
if [ -n "$SESSION_ID" ] && [ -f "$DEDUP_DIR/skill-hint-$SESSION_ID" ]; then
exit 0 # このセッションでは既に注入済みのためスキップ
fi
# ============================================
# プロジェクトで定めた skill 利用ルールを読み込む(プロジェクトごとにカスタマイズ可能)
SKILL_HINT=""
# === 以下はプロジェクトの実情に合わせて変更してください ===
# 例1:常に git-commit skill の利用を促す
# SKILL_HINT="ユーザーがコードのコミットを求めた場合は /git-commit skill を使用してください"
# ============================================
if [ -z "$SKILL_HINT" ]; then
exit 0
fi
# このセッションで注入済みであることを記録
[ -n "$SESSION_ID" ] && touch "$DEDUP_DIR/skill-hint-$SESSION_ID"
cat <<EOF
{"hookSpecificOutput": {"additionalContext": "$SKILL_HINT"}}
EOF
exit 0
要点:
additionalContext はユーザーの Prompt に追記され、system-reminder として Agent に注入されます
session_id と一時ファイルによるセッション単位の重複排除で、毎ターン同じヒントが注入されるのを防ぎます
- スクリプトからプロジェクトの設定ファイルを読み込み、プロジェクト固有の Skill を推奨できます
exit 0 で Prompt を通過させ、exit 2 でブロックします(規約に違反する Prompt など)
シナリオ 2:機密情報を含む Prompt の遮断
課題: ユーザーが Prompt にパスワード、キー、内部 IP、個人情報を誤って含めてしまい、情報漏洩のリスクがある。
解決策: UserPromptSubmit Hook で機密情報を検出し、Prompt をブロックします。
設定:
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": ".qoder/hooks/block-sensitive-prompt.sh"
}
]
}
]
}
}
スクリプト .qoder/hooks/block-sensitive-prompt.sh:
#!/bin/sh
# 機能:ユーザー Prompt 内の機密情報を検出し、該当すればブロックする
INPUT=$(cat)
# ユーザー Prompt の内容を取得
PROMPT=$(printf '%s' "$INPUT" | jq -r '.prompt // empty')
if [ -z "$PROMPT" ]; then
exit 0
fi
# === 機密語のルールはチームのセキュリティ規約に合わせて調整してください ===
# 1. キー / 認証情報のパターン
SECRET_PATTERNS="password=|passwd=|secret_key=|access_key=|AKIA[0-9A-Z]{16}|token=[a-zA-Z0-9]{20,}"
# 2. 内部ネットワーク情報
INTERNAL_PATTERNS="10\.[0-9]+\.[0-9]+\.[0-9]+|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\."
# 3. カスタムの機密語(チーム内の用語、プロジェクトのコードネームなど)
# CUSTOM_PATTERNS="プロジェクトコードネームX|内部 API アドレス"
# すべてのパターンを結合
ALL_PATTERNS="$SECRET_PATTERNS|$INTERNAL_PATTERNS"
# 検出を実行
MATCH=$(echo "$PROMPT" | grep -oiE "$ALL_PATTERNS" | head -1)
if [ -n "$MATCH" ]; then
echo "機密情報を検出しました: $MATCH" >&2
exit 2 # Prompt の送信をブロック
fi
# ============================================
exit 0
要点:
UserPromptSubmit はブロック可能なイベントであり、exit 2 で Prompt が Agent に届く前に遮断できます
- 機密情報のパターンは正規表現に対応しており、柔軟なマッチングが可能です(AWS の AKIA プレフィックス、内部 IP レンジなど)
- パターンを別の設定ファイル(例:
.qoder/hooks/sensitive-patterns.txt)に切り出すと、チームで保守しやすくなります
- より精度の高い検出が必要な場合は、
gitleaks や trufflehog などの外部ツールを呼び出してください
シナリオ 1 との違い:シナリオ 1 は exit 0 と additionalContext による Prompt の強化(コンテキストの注入)ですが、本シナリオは exit 2 による Prompt のブロック(規約に違反する入力の拒否)です。両者は同じ UserPromptSubmit イベントに共存でき、設定順に実行されます。
シナリオ 3:Rule / Skill の利用状況分析
課題: 多数の Rules や Skills を設定しているが、実際の利用率が分からない。
解決策: Transcript システムと Stop Hook を組み合わせ、会話ごとに Rule / Skill の発火データを自動で分析・記録します。
設定:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": ".qoder/hooks/analyze-rule-skill-usage.sh"
}
]
}
]
}
}
スクリプト .qoder/hooks/analyze-rule-skill-usage.sh:
#!/bin/sh
# 機能:Agent の応答完了時に、このセッションの Rule / Skill 利用状況を分析する
INPUT=$(cat)
# stdin からフィールドを取得
TRANSCRIPT_PATH=$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty')
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty')
if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
exit 0
fi
# === 📝 分析ロジック — 実際の要件に合わせて調整してください ===
# 1. session_meta 内の rules 情報を取得(JSONL を 1 行ずつ解析)
RULES_META=$(jq -c 'select(.data.meta_type == "rules") | .data.content' "$TRANSCRIPT_PATH" 2>/dev/null | head -1)
# 2. slash_command 情報を取得(ユーザーが / で起動した Skill)
SLASH_SKILL_META=$(jq -c 'select(.data.meta_type == "slash_command") | .data.content' "$TRANSCRIPT_PATH" 2>/dev/null | head -1)
# 3. tool_name="Skill" の呼び出しを取得(Agent 自身が起動した Skill ツール呼び出し)
# 補足:ユーザーが手動で /skill-name を実行する以外に、Agent 自身も Skill ツールを呼び出します
AUTO_SKILL_CALLS=$(jq -c 'select(.message.content[ ]? | select(.type == "tool_use" and .name == "Skill"))' "$TRANSCRIPT_PATH" 2>/dev/null)
AUTO_SKILL_COUNT=$(printf '%s' "$AUTO_SKILL_CALLS" | grep -c . 2>/dev/null || echo 0)
AUTO_SKILL_NAMES=$(printf '%s' "$AUTO_SKILL_CALLS" | jq -r '.message.content[ ] | select(.type == "tool_use" and .name == "Skill") | .input.skill' 2>/dev/null | sort -u | jq -R -s 'split("\n") | map(select(. != ""))')
# 4. すべてのツール呼び出し回数を集計
TOOL_COUNT=$(jq -c 'select(.message.content[ ]?.type == "tool_use")' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')
# 5. 統計ファイルへ記録
STATS_DIR="$HOME/.qoder/stats"
mkdir -p "$STATS_DIR"
DATE=$(date +%Y-%m-%d)
STATS_FILE="$STATS_DIR/usage-${DATE}.jsonl"
jq -n --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --arg sid "$SESSION_ID" \
--argjson tc "$TOOL_COUNT" \
--argjson rules "${RULES_META:-null}" \
--argjson slash_skill "${SLASH_SKILL_META:-null}" \
--argjson auto_skill_count "$AUTO_SKILL_COUNT" \
--argjson auto_skill_names "${AUTO_SKILL_NAMES:-null}" \
'{timestamp:$ts, session_id:$sid, tool_calls:$tc, rules:$rules, slash_skill:$slash_skill, auto_skill: {count:$auto_skill_count, names:$auto_skill_names}}' >> "$STATS_FILE"
# ============================================
exit 0
Transcript が自動で記録するメタデータ:
- session_meta(rules): このセッションで読み込まれたすべての Rules(名前、トリガー種別、ファイルパス)
- session_meta(slash_command): このセッションで使用された Skills(名前、種別、ファイルパス)
- session_meta(session_info): セッションのモード(agent / plan)と種別
応用:定期実行の集計スクリプトを書いて ~/.qoder/stats/ の JSONL ファイルを読み込み、Rule / Skill の利用状況レポートを生成できます。
シナリオ 4:ファイル編集の追跡
課題: セッション中に Agent がどのファイルを何回変更したのかが分からない。
解決策: ファイル編集系ツールにマッチする PostToolUse Hook で、すべてのファイル変更をリアルタイムに記録します。
設定:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|search_replace|create_file",
"hooks": [
{
"type": "command",
"command": ".qoder/hooks/track-file-changes.sh"
}
]
}
]
}
}
スクリプト .qoder/hooks/track-file-changes.sh:
#!/bin/sh
# 機能:Agent のファイル編集操作を追跡する
INPUT=$(cat)
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty')
TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty')
FILE_PATH="$QODER_TOOL_INPUT_FILE_PATH"
if [ -z "$FILE_PATH" ]; then
exit 0
fi
# === 📝 ファイル変更の追跡ロジック ===
# 変更ログを記録
CHANGE_LOG="$HOME/.qoder/stats/file-changes.jsonl"
mkdir -p "$(dirname "$CHANGE_LOG")"
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
BRANCH=$(printf '%s' "$INPUT" | jq -r '.extra.branch // empty')
REPO=$(printf '%s' "$INPUT" | jq -r '.extra.repo // empty')
echo "{\"ts\":\"$TIMESTAMP\",\"session\":\"$SESSION_ID\",\"tool\":\"$TOOL_NAME\",\"file\":\"$FILE_PATH\",\"branch\":\"$BRANCH\",\"repo\":\"$REPO\"}" >> "$CHANGE_LOG"
# ============================================
exit 0
応用:
additionalContext で変更の統計を Agent にフィードバックできます(例: 「このセッションで 15 ファイルを変更」)
- チームのコード分析システムと連携し、AI 支援による変更量を追跡できます
シナリオ 5:全体的な利用状況分析
課題: Agent の利用状況に関する定量的なデータがなく、AI 支援開発の効率と品質を評価できない。具体的には、ユーザーがどんな質問をしたか、モデルがどんなテキストを返したか、どのツールが呼び出されて結果はどうだったか、といった情報が分からない。
解決策: 複数イベントの Hook を組み合わせて、利用データの全パイプライン収集を構築します。UserPromptSubmit でユーザーの質問を、PostToolUse でツール呼び出しの結果を収集し、Stop で Transcript からセッション全体のサマリー(モデルの応答、ツール呼び出しの分布など)を分析します。
設定:
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "~/.qoder/hooks/usage-tracker.sh"
}
]
}
],
"PostToolUse": [
{
"hooks": [
{
"type": "command",
"command": "~/.qoder/hooks/usage-tracker.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "~/.qoder/hooks/usage-tracker.sh"
}
]
}
]
}
}
スクリプト ~/.qoder/hooks/usage-tracker.sh:
#!/bin/sh
# 機能:全体的な利用状況の追跡(統一の入口でイベント種別ごとに振り分け)
INPUT=$(cat)
EVENT=$(printf '%s' "$INPUT" | jq -r '.hook_event_name // empty')
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty')
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
# 追加のコンテキストを取得
EMAIL=$(printf '%s' "$INPUT" | jq -r '.extra.email // empty')
REPO=$(printf '%s' "$INPUT" | jq -r '.extra.repo // empty')
BRANCH=$(printf '%s' "$INPUT" | jq -r '.extra.branch // empty')
# === 📝 データ収集ロジック ===
STATS_DIR="$HOME/.qoder/stats"
mkdir -p "$STATS_DIR"
DATE=$(date +%Y-%m-%d)
case "$EVENT" in
UserPromptSubmit)
# ユーザーの質問内容を収集(ログが大きくなりすぎないよう先頭 200 文字のみ)
PROMPT=$(printf '%s' "$INPUT" | jq -r '.prompt // empty')
PROMPT_PREVIEW=$(printf '%.200s' "$PROMPT")
echo "{\"ts\":\"$TIMESTAMP\",\"event\":\"prompt\",\"session\":\"$SESSION_ID\",\"email\":\"$EMAIL\",\"repo\":\"$REPO\",\"branch\":\"$BRANCH\",\"prompt_preview\":\"$PROMPT_PREVIEW\"}" >> "$STATS_DIR/events-${DATE}.jsonl"
;;
PostToolUse)
TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty')
# ツール呼び出しの結果を収集(先頭 500 文字のみ)
TOOL_RESPONSE=$(printf '%s' "$INPUT" | jq -r '.tool_response // empty')
TOOL_RESPONSE_PREVIEW=$(printf '%.500s' "$TOOL_RESPONSE")
echo "{\"ts\":\"$TIMESTAMP\",\"event\":\"tool_use\",\"session\":\"$SESSION_ID\",\"tool\":\"$TOOL_NAME\",\"repo\":\"$REPO\",\"tool_response_preview\":\"$TOOL_RESPONSE_PREVIEW\"}" >> "$STATS_DIR/events-${DATE}.jsonl"
;;
Stop)
# 📊 Transcript からセッション全体のサマリーを収集
TRANSCRIPT_PATH=$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty')
SUMMARY=""
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
USER_MSG_COUNT=$(jq -c 'select(.type == "user" and (.message.content | type == "string"))' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')
USER_PROMPTS=$(jq -r 'select(.type == "user" and (.message.content | type == "string")) | .message.content' "$TRANSCRIPT_PATH" 2>/dev/null | head -20)
ASSISTANT_TEXT_COUNT=$(jq -c 'select(.type == "assistant" and (.message.content[ ]? | select(.type == "text")))' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')
ASSISTANT_TEXTS=$(jq -r 'select(.type == "assistant") | .message.content[ ]? | select(.type == "text") | .text' "$TRANSCRIPT_PATH" 2>/dev/null | head -20)
TOOL_CALL_COUNT=$(jq -c 'select(.type == "assistant") | .message.content[ ]? | select(.type == "tool_use")' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')
TOOL_NAMES=$(jq -r 'select(.type == "assistant") | .message.content[ ]? | select(.type == "tool_use") | .name' "$TRANSCRIPT_PATH" 2>/dev/null | sort | uniq -c | sort -rn | head -10)
TOOL_SUCCESS=$(jq -c 'select(.type == "user") | .message.content[ ]? | select(.type == "tool_result" and .is_error == false)' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')
TOOL_ERROR=$(jq -c 'select(.type == "user") | .message.content[ ]? | select(.type == "tool_result" and .is_error == true)' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')
SUMMARY="user_msgs:${USER_MSG_COUNT},assistant_texts:${ASSISTANT_TEXT_COUNT},tool_calls:${TOOL_CALL_COUNT},tool_success:${TOOL_SUCCESS},tool_error:${TOOL_ERROR}"
fi
echo "{\"ts\":\"$TIMESTAMP\",\"event\":\"stop\",\"session\":\"$SESSION_ID\",\"repo\":\"$REPO\",\"summary\":\"$SUMMARY\"}" >> "$STATS_DIR/events-${DATE}.jsonl"
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
SUMMARY_DIR="$STATS_DIR/sessions"
mkdir -p "$SUMMARY_DIR"
cat <<SUMMARY_EOF > "$SUMMARY_DIR/${SESSION_ID}.json"
{
"session_id": "$SESSION_ID",
"timestamp": "$TIMESTAMP",
"repo": "$REPO",
"branch": "$BRANCH",
"email": "$EMAIL",
"user_message_count": $USER_MSG_COUNT,
"assistant_text_count": $ASSISTANT_TEXT_COUNT,
"tool_call_count": $TOOL_CALL_COUNT,
"tool_success": $TOOL_SUCCESS,
"tool_error": $TOOL_ERROR,
"tool_distribution": "$TOOL_NAMES",
"user_prompts_preview": $(printf '%s' "$USER_PROMPTS" | head -c 2000 | jq -Rs .),
"assistant_texts_preview": $(printf '%s' "$ASSISTANT_TEXTS" | head -c 2000 | jq -Rs .),
"transcript_path": "$TRANSCRIPT_PATH"
}
SUMMARY_EOF
fi
;;
esac
# ============================================
exit 0
データ収集の対象:
| イベント | 収集するデータ | 取得元 |
|---|
UserPromptSubmit | ユーザーの質問(先頭 200 文字) | stdin JSON の prompt フィールド |
PostToolUse | ツール名 + 実行結果(先頭 500 文字) | stdin JSON の tool_name / tool_response フィールド |
Stop | セッション全体のサマリー: ユーザーの質問、モデルの応答、ツール呼び出しの分布、成功 / 失敗率 | transcript_path の JSONL ファイル |
なぜ Stop で Transcript を分析するのか:UserPromptSubmit と PostToolUse はその時点のやり取りしか取得できませんが、Stop の時点の Transcript ファイルにはセッションの全履歴が含まれているため、モデルの応答テキスト、ツール呼び出しの分布、成功 / 失敗率を一度に抽出できます。詳細は Transcript ファイル形式 を参照してください。
シナリオ 6:安全性の制御 — 危険コマンドの遮断
課題: Agent が rm -rf や git push --force などの危険なコマンドを実行してしまう可能性がある。
解決策: Bash|run_in_terminal にマッチする PreToolUse Hook で、実行前に危険なコマンドを遮断します。
設定:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|run_in_terminal",
"hooks": [
{
"type": "command",
"command": ".qoder/hooks/block-dangerous-commands.sh"
}
]
}
]
}
}
スクリプト .qoder/hooks/block-dangerous-commands.sh:
#!/bin/sh
# 機能:危険なシェルコマンドを遮断する
INPUT=$(cat)
# 実行されるコマンドを取得(tool_input.command から読み取る)
COMMAND=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty')
# === 📝 危険コマンドのブラックリスト — チームの規約に合わせて調整してください ===
DANGEROUS_PATTERNS="rm -rf|git push --force|git push -f|DROP TABLE|DROP DATABASE|format |mkfs"
if echo "$COMMAND" | grep -qiE "$DANGEROUS_PATTERNS"; then
echo "危険なコマンドを検出しました: $COMMAND" >&2
exit 2 # 実行をブロック
fi
# ============================================
exit 0
要点:
exit 2 で実行を即座にブロックし、stderr の内容がブロック理由として Agent にフィードバックされます
- Agent はブロックされた後、代替手段(より安全なコマンドなど)を試みます
- より詳細なフィードバックが必要な場合は、stdout に JSON を返します:
{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"Command contains rm -rf, blocked for safety"}}
シナリオ 7:Agent 完了前の品質ゲート
課題: Agent はタスク完了を報告するが、実際にはテストが失敗していたり問題が残っている。
解決策: ブロック可能な Stop Hook で、Agent が完了する前に品質チェックを実行し、チェックに失敗した場合はブロックして Agent に作業を継続させます。
設定:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": ".qoder/hooks/quality-gate.sh",
"timeout": 120
}
]
}
]
}
}
スクリプト .qoder/hooks/quality-gate.sh:
#!/bin/sh
# 機能:Agent 完了前の品質ゲートチェック
INPUT=$(cat)
# Stop Hook によって発生したループかどうかを確認(無限ループの防止)
STOP_HOOK_ACTIVE=$(printf '%s' "$INPUT" | jq -r '.stop_hook_active // false')
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
exit 0 # Stop Hook による再試行なので通過させる
fi
# === 📝 品質チェックのロジック — プロジェクトの実情に合わせて調整してください ===
ERRORS=""
# 1. コンパイルエラーがないか確認
# if ! make build 2>/dev/null; then
# ERRORS="${ERRORS}\n- ビルドに失敗しました。コンパイルエラーを修正してください"
# fi
# 2. 失敗しているテストがないか確認
# if ! make test 2>/dev/null; then
# ERRORS="${ERRORS}\n- テストが通っていません。失敗したテストを修正してください"
# fi
# 3. 未対応の TODO マーカーが残っていないか確認
# TODO_COUNT=$(grep -r "TODO(agent)" . --include="*.go" 2>/dev/null | wc -l | tr -d ' ')
# if [ "$TODO_COUNT" -gt 0 ]; then
# ERRORS="${ERRORS}\n- 未完了の TODO マーカーが $TODO_COUNT 件見つかりました"
# fi
# ============================================
if [ -n "$ERRORS" ]; then
cat <<EOF
{"decision":"block","reason":"品質チェックに失敗しました:$ERRORS\n上記の問題を修正してから完了してください。"}
EOF
exit 2
fi
exit 0
要点:
stop_hook_active は無限ループを防ぐために使用します(ブロック後に Agent が再試行している場合は true になります)
- Stop Hook がブロックすると、ブロック理由がユーザーメッセージとして注入され、Agent は作業を継続します
- ビルドやテストには時間がかかるため、
timeout は長め(例: 120 秒)に設定してください
シナリオ 8:Harness の自己進化 — 知識の自動蓄積
課題: タスクごとの知見や判断が会話履歴に散在しており、自動で蓄積する仕組みがない。
解決策: Stop Hook で Harness の自己進化フローを自動的にトリガーし、その会話に再利用できる知見があるかを分析して資産のライフサイクルを回します。
設定:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": ".qoder/hooks/harness-evolution.sh"
}
]
}
]
}
}
スクリプト .qoder/hooks/harness-evolution.sh:
#!/bin/sh
# 機能:Agent の応答完了時に Harness の自己進化フローをトリガーする
INPUT=$(cat)
# ⚠️ 【重要】無限ループの防止:stop_hook_active フラグを確認する
# Stop Hook が Agent をブロックすると Agent は完了を再試行し、その際 stop_hook_active=true になります
# ここで必ず通過させないと「ブロック→再試行→再ブロック」の無限ループに陥ります
STOP_HOOK_ACTIVE=$(printf '%s' "$INPUT" | jq -r '.stop_hook_active // false')
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
exit 0 # Stop Hook による再試行なのでそのまま通過させる
fi
TRANSCRIPT_PATH=$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty')
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty')
if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
exit 0
fi
# === 📝 Harness 自己進化の検出ロジック ===
# 1. このセッションのツール呼び出しとファイル変更を集計
EDIT_COUNT=$(jq -c 'select(.message.content[ ]?.type == "tool_use")' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')
# 2. アーキテクチャ設計、意思決定の議論、規約策定などのキーワードが含まれるか確認
# HAS_DECISION=$(jq -r 'select(.message.content | type == "string") | .message.content' "$TRANSCRIPT_PATH" 2>/dev/null | grep -c 'アーキテクチャ\|方針\|規約\|ベストプラクティス' || echo 0)
# 3. セッションのサマリーを inbox に記録し、後でまとめて振り返る
SEDIMENTATION_DIR="$HOME/.ai/inbox"
mkdir -p "$SEDIMENTATION_DIR"
echo "{\"session_id\":\"$SESSION_ID\",\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"tool_calls\":$EDIT_COUNT,\"transcript\":\"$TRANSCRIPT_PATH\"}" >> "$SEDIMENTATION_DIR/pending-review.jsonl"
# 4. 選択肢 A:外部の分析・要約システムを呼び出す
# transcript とセッションコンテキストを使って自前の分析サービスを呼び出す
# curl -s -X POST http://localhost:8080/api/analyze \
# -H 'Content-Type: application/json' \
# -d "{\"session_id\":\"$SESSION_ID\",\"transcript\":\"$TRANSCRIPT_PATH\"}" \
# > /dev/null 2>&1 &
# 5. 選択肢 B:ブロックによって分析・要約 Skill をトリガーする
# 以下のコメントを解除すると、Agent がブロックされた後に自動で振り返り Skill のフローへ入ります
# 注意:/retro はカスタムの振り返り Skill です。自チームの振り返り Skill に置き換えてください。
# cat <<'EOF'
# {"decision":"block","reason":"タスクが完了しました。この会話には蓄積する価値のある知見がある可能性を検出しました(ファイル変更が多いため)。/retro を実行して振り返りを行い、再利用できる知見を抽出して資産体系に蓄積してください。"}
# EOF
# exit 2
# ============================================
exit 0
要点:
- 無限ループの防止(必須): Stop Hook のスクリプトでは必ず
stop_hook_active を確認してください。Stop Hook にブロックされた Agent が再試行している場合、このフィールドは true になるため、即座に exit 0 して無限ループを回避します
exit 2 と decision:"block" で Agent の完了をブロックし、振り返りを強制できます
pending-review.jsonl にセッションを記録しておき、後でまとめてレビューできます
/retro Skill(カスタムの振り返り Skill)と組み合わせることで、自動検出 → リマインド → 蓄積のクローズドループを構成できます
現時点での実装方法:
Hooks は現在 command と http タイプのハンドラのみをサポートしているため(prompt / agent は非対応)、Harness の自己進化には 2 つの実装パスがあります。(A) 外部の分析サービスを呼び出す、(B) Agent をブロックして振り返り Skill をトリガーする。
今後の方向性:
Hook システムは prompt タイプと agent タイプのハンドラのサポートを予定しています。これらが利用可能になれば、Harness の自己進化は「スクリプト駆動」から「Agent 駆動」へと進化し、真にエンドツーエンドで自動化された知識の蓄積が実現します。
シナリオ 9:ファイル書き込み後の自動 Lint
Agent がファイルを書き込み・編集するたびに、自動で lint を実行します。
スクリプト ${project}/.qoder/hooks/auto-lint.sh:
#!/bin/bash
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path')
# Only lint JS/TS files
case "$file_path" in
*.js|*.ts|*.jsx|*.tsx)
npx eslint "$file_path" --fix 2>/dev/null
;;
esac
exit 0
設定: イベント PostToolUse、matcher Write|Edit,command .qoder/hooks/auto-lint.sh。
シナリオ 10:ツール失敗時のログ記録
ツールの実行が失敗した際に、自動でログファイルに記録して問題の調査に役立てます。
スクリプト ~/.qoder/hooks/log-failure.sh:
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')
error=$(echo "$input" | jq -r '.error')
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] $tool_name failed: $error" >> ~/.qoder/hooks/failure.log
exit 0
設定: イベント PostToolUseFailure、matcher なし(すべてのツールにマッチ),command ~/.qoder/hooks/log-failure.sh。
シナリオ 11:Agent 完了時のデスクトップ通知
Agent がタスクを完了したらシステム通知を表示します。長時間かかるタスクに便利です。
スクリプト ~/.qoder/hooks/notify-done.sh(macOS):
#!/bin/bash
input=$(cat)
message=$(echo "$input" | jq -r '.last_assistant_message // "Task complete"' | head -c 100)
osascript -e "display notification \"$message\" with title \"Qoder IDE Agent\""
exit 0
設定: イベント Stop、matcher なし,command ~/.qoder/hooks/notify-done.sh。
Transcript ファイル形式
Transcript は Qoder IDE が自動生成するセッションログファイルで、transcript_path が指すパス(例: ~/.qoder/projects/<project>/transcript/<session-id>.jsonl)にあります。各行は独立した JSON オブジェクトで、時系列順に追記され、セッションのやり取りの全体が記録されます。
各行の共通フィールド
| フィールド | 型 | 説明 |
|---|
type | string | レコード種別: session_meta / user / assistant / progress |
sessionId | string | セッション ID |
uuid | string | このレコードの一意な ID |
timestamp | string | ISO 8601 形式のタイムスタンプ |
cwd | string | 現在の作業ディレクトリ |
message | object | メッセージの内容(user / assistant 種別に存在) |
data | object | メタデータ(session_meta / progress 種別に存在) |
レコード種別
1. session_meta — セッションのメタデータ(1 行目)
Transcript ファイルの 1 行目には、セッションの基本情報が記録されます:
{
"type": "session_meta",
"sessionId": "86379a0e-...",
"data": {
"meta_type": "session_info",
"content": {
"mode": "agent",
"session_type": "assistant"
}
}
}
data.content.mode: セッションのモード(agent / plan / ask / debug)
data.content.session_type: セッションの種別(assistant / inline_chat など)
2. user — ユーザーメッセージ
ユーザーメッセージには 2 つの形式があります。
ユーザーの質問(message.content が文字列):
{
"type": "user",
"message": {
"role": "user",
"content": "Delete comments from test.py"
}
}
ツールの実行結果(message.content が tool_result を含む配列):
{
"type": "user",
"message": {
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "call_d498c5988a...",
"content": "Contents of /path/to/file.py, from line 1-23 ...",
"is_error": false
}
]
},
"toolUseResult": "Contents of /path/to/file.py ..."
}
is_error: ツールの実行が失敗したかどうか
toolUseResult: ツールの実行結果へのショートカットフィールド(content[0].content と同じ)
3. assistant — モデルの応答
モデルの応答の message.content は常に配列で、2 種類の要素を含みます。
テキスト応答(type: "text"):
{
"type": "assistant",
"message": {
"role": "assistant",
"content": [
{
"type": "text",
"text": "I'll help you delete comments from test.py. Let me read the file first.\n\n"
}
]
}
}
ツール呼び出し(type: "tool_use"):
{
"type": "assistant",
"message": {
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "call_d498c5988a...",
"name": "read_file",
"input": {
"file_path": "/path/to/file.py"
}
}
]
}
}
name: ツール名(例: read_file、search_replace、run_in_terminal、Skill)
input: ツール呼び出しのパラメータ
id: 後続の tool_result の tool_use_id に対応
4. progress — Hook の発火記録
Hook スクリプトがいつ発火し、どのコマンドを実行したかが記録されます:
{
"type": "progress",
"data": {
"type": "hook_progress",
"hookEvent": "UserPromptSubmit",
"hookName": "UserPromptSubmit",
"command": ".qoder/hooks/inject-skill-hint.sh"
}
}
セッションのタイムライン例
典型的なセッションにおける Transcript のレコード順序:
L1 session_meta ← Session start, record mode and type
L2 progress ← UserPromptSubmit Hook fires
L3 user (string) ← User question: "Delete comments from test.py"
L4 assistant (text) ← Model reply: "I'll help you..."
L5 assistant (tool_use) ← Model calls read_file
L6 user (tool_result) ← read_file returns file content
L7 assistant (text) ← Model reply: "Now I'll delete..."
L8 assistant (tool_use) ← Model calls search_replace
L9 user (tool_result) ← search_replace succeeds
L10 assistant (text) ← Model reply: "Successfully deleted..."
L11 progress ← Stop Hook fires
L12 assistant (text) ← Final reply after Stop Hook
よく使う jq 抽出コマンド
TRANSCRIPT="$TRANSCRIPT_PATH"
# Extract all user questions (filter out tool results)
jq -r 'select(.type == "user" and (.message.content | type == "string")) | .message.content' "$TRANSCRIPT"
# Extract all model text replies
jq -r 'select(.type == "assistant") | .message.content[ ]? | select(.type == "text") | .text' "$TRANSCRIPT"
# Extract all tool calls (tool name + parameters)
jq -c 'select(.type == "assistant") | .message.content[ ]? | select(.type == "tool_use") | {name, input}' "$TRANSCRIPT"
# Count tool call distribution (tool name + count)
jq -r 'select(.type == "assistant") | .message.content[ ]? | select(.type == "tool_use") | .name' "$TRANSCRIPT" | sort | uniq -c | sort -rn
# Extract failed tool calls
jq -c 'select(.type == "user") | .message.content[ ]? | select(.type == "tool_result" and .is_error == true)' "$TRANSCRIPT"
# Get session mode
jq -r 'select(.type == "session_meta") | .data.content.mode' "$TRANSCRIPT"
# Get Hook trigger records
jq -c 'select(.type == "progress" and .data.type == "hook_progress") | {event: .data.hookEvent, command: .data.command}' "$TRANSCRIPT"
設計原則
| 原則 | 説明 |
|---|
| フェイルファスト | Hook スクリプトは軽量に保ち、Agent のメインフローをブロックしない |
| グレースフルデグラデーション | 異常な exit code(0 でも 2 でもない場合)では Agent をブロックせず、耐障害性を確保する |
| 単一責任 | 1 つのスクリプトは 1 つのことだけを行い、複雑なロジックは複数の Hook を組み合わせて実現する |
| 冪等な設計 | 同じイベントが複数回発火する可能性があるため、スクリプトは冪等に作る |
| ループの防止 | Stop Hook では stop_hook_active を確認し、無限に再試行されるのを防ぐ |
推奨される Hook の組み合わせ
| 目的 | 推奨する組み合わせ |
|---|
| 安全性の制御 | PreToolUse(Bash) による危険コマンドの遮断 |
| 品質の担保 | Stop による品質ゲート |
| データドリブンな分析 | UserPromptSubmit + PostToolUse + Stop による全パイプライン収集 |
| Harness の自己進化 | Stop による蓄積の検出 + UserPromptSubmit による Skill の誘導 |
| チームでの協業 | プロジェクトレベルの .qoder/settings.json で設定を統一し、Git で共有 |
デバッグガイド
ステップ 1: Hook が発火しているか確認する
Hook が想定どおりに発火しない場合は、スクリプトの先頭にデバッグログを追加します:
#!/bin/sh
# ===== Debug mode: confirm hook fires =====
INPUT=$(cat)
printf '[HOOK DEBUG] %s hook triggered, event=%s\n' "$(date '+%H:%M:%S')" "$(printf '%s' "$INPUT" | jq -r '.hook_event_name')" >> /tmp/hook-debug.log
printf '%s' "$INPUT" | jq . >> /tmp/hook-debug.log
echo "---" >> /tmp/hook-debug.log
# ===== Remove debug code when done =====
# ... your actual logic ...
exit 0
続いて Agent の操作をトリガーし、ログを確認します:
$ tail -f /tmp/hook-debug.log
[HOOK DEBUG] 14:32:01 hook triggered, event=PreToolUse
{
"session_id": "abc123",
"hook_event_name": "PreToolUse",
"tool_name": "run_in_terminal",
"tool_input": {
"command": "ls -la"
},
...
}
トラブルシューティング:何も出力されない場合、Hook は発火していません。設定ファイルの場所、イベント名、matcher のパターン、スクリプトのパスを確認してください。
ステップ 2: 実際の入力でローカル再現する
ステップ 1 で取得した JSON を使い、Qoder IDE の外でスクリプトをテストします:
# Pipe the captured input to simulate stdin
$ cat /tmp/hook-debug.json | sh .qoder/hooks/pre-tool-check.sh
$ echo $? # Check exit code: 0=allow, 2=block
# Or manually construct test input
$ echo '{"hook_event_name":"PreToolUse","tool_name":"run_in_terminal","tool_input":{"command":"rm -rf /"}}' | sh .qoder/hooks/pre-tool-check.sh
$ echo $? # Expected: 2 (block dangerous command)
ヒント:よく使うテストケースはファイルとして保存しておくと、繰り返し検証できます。
ステップ 3: その他のデバッグ方法
- Transcript を確認する:
~/.qoder/projects/<encoded-path>/transcript/<session>.jsonl を jq で 1 行ずつ解析する
- Hook のログを確認する: Qoder IDE のログで
[hook] プレフィックスを検索し、実行結果と所要時間を確認する
- 小さく始める: まず
exit 0 だけのスクリプトで発火を確認し、その後で業務ロジックを段階的に追加する
- 後片付け: デバッグが終わったらデバッグコード(
/tmp/hook-debug.log への書き込み)を削除し、パフォーマンスへの影響を避ける
クイックスタートテンプレート
最小構成の設定
以下の内容を ~/.qoder/settings.json(ユーザーレベル)または <project>/.qoder/settings.json(プロジェクトレベル)に保存します:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".qoder/hooks/pre-tool-check.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": ".qoder/hooks/post-edit-track.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": ".qoder/hooks/on-stop.sh"
}
]
}
]
}
}
プロジェクトのディレクトリ構成
<project>/
├── .qoder/
│ ├── settings.json ← Hook config (Git-shared)
│ ├── settings.local.json ← Local override config (.gitignore)
│ └── hooks/ ← Hook scripts directory
│ ├── pre-tool-check.sh
│ ├── post-edit-track.sh
│ └── on-stop.sh
└── ...
完全な設定例
よく使われる 5 つのイベントに Hook を設定した完全な例です。
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "~/.qoder/hooks/check-prompt.sh"
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.qoder/hooks/block-dangerous.sh"
}
]
},
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": ".qoder/hooks/validate-file-path.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": ".qoder/hooks/auto-lint.sh"
}
]
}
],
"PostToolUseFailure": [
{
"hooks": [
{
"type": "command",
"command": "~/.qoder/hooks/log-failure.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "~/.qoder/hooks/notify-done.sh"
}
]
}
]
}
}