Skip to main content
拡張機能

Hooks

Hooks を使うと、Qoder IDE や JetBrains プラグインの実行フローにおける重要なポイントで、コードを変更することなくカスタムロジックを挿入できます。JSON 設定ファイルを編集するだけで、たとえば次のようなことを実現できます。
  • ツール実行前に危険な操作をブロック
  • ファイル書き込みのたびに自動で lint を実行し、コードスタイルを統一
  • Agent のタスク完了時にデスクトップ通知を送り、画面に張り付く必要をなくす
Prompt による指示とは異なり、Hooks は確定的に動作します。イベントが発火すればスクリプトは必ず実行され、モデルの解釈に結果が左右されることはありません。
Hooks の機能は入口ごとに異なります。本ページは Qoder IDE / JetBrains プラグイン(12 イベント、commandhttp の 2 タイプのみ)に対応します。Qoder CLI HooksQoderWork Hooks は別ページで説明しています。IDE と CLI は同じ設定ファイルを共有しますが、各入口は自身がサポートするイベントのみを実行します。

サポートされるイベント

IDE / JB プラグインが現在サポートしている Hook イベントは以下の 12 種類です。
イベント名発火タイミングブロック可能
SessionStartセッションの開始時または再開時いいえ
UserPromptSubmitユーザーが Prompt を送信した後、Agent が処理を開始する前はい
PreToolUseツール実行の前はい
PermissionRequestツールにユーザーの承認が必要になった時はい
PostToolUseツール実行の成功後いいえ
PostToolUseFailureツール実行の失敗後いいえ
SubagentStartサブエージェントの開始時いいえ
SubagentStopサブエージェントの停止時いいえ
StopAgent がレスポンスを返し終えた時はい
SessionEndセッションの終了時いいえ
PreCompactコンテキスト圧縮の前いいえ
Notificationユーザー向け通知が発行された時いいえ

クイックスタート

以下の例では、rm -rf のような危険なコマンドをブロックする方法を紹介します。
1

スクリプトを作成

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
2

設定を追加

~/.qoder/settings.json に以下を追加します。
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.qoder/hooks/block-rm.sh"
          }
        ]
      }
    ]
  }
}
3

動作を確認

IDE を開き、Qoder プラグインパネルで rm -rf を含むコマンドの実行を Agent に指示してください。Hook が実行をブロックし、エラーメッセージを Agent にフィードバックします。

ユースケース

シナリオ対応イベント説明
危険コマンドの遮断PreToolUserm -rfDROP TABLE などの実行前にブロック
ファイルパスの検証PreToolUse指定ディレクトリ内でのみファイルの作成・編集を許可
自動 Lint / フォーマットPostToolUseファイル書き込みのたびに ESLint / Prettier を自動実行
監査ログPostToolUseすべてのツール呼び出しを記録し、セキュリティ監査に活用
失敗時のモニタリングPostToolUseFailureツール実行が失敗した際にアラートを送信、またはエラーログを記録
Prompt の内容チェックUserPromptSubmitユーザー入力に機密情報(パスワード、キーなど)が含まれていないか検出
コンテキストの自動注入UserPromptSubmitPrompt の末尾にプロジェクト規約やコーディング規約を自動付加
デスクトップ通知StopAgent 完了時にシステム通知を表示

仕組み

Hook の利用は、スクリプトを書く → 設定に登録する → 自動で有効になる、という 3 ステップです。 Agent がライフサイクル上の特定のポイント(例: ツール呼び出し前)に到達すると、プラグインは該当するイベントに Hook が登録されているかを確認します。
  1. プラグインは起動時にすべての Hook 設定を読み込む
  2. Agent の実行中にイベントポイント(例: PreToolUse)に到達する
  3. プラグインはそのイベントに登録された Hook グループを順に走査し、matcher で現在のコンテキストと照合する
  4. マッチした Hook は登録順にシェルスクリプトを実行する
  5. スクリプトは stdin からイベントコンテキスト(JSON)を受け取り、exit code と stdout で結果を返す
  6. プラグインはその結果に基づいて後続の動作(続行またはブロック)を決定する

前提条件

  • 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_terminalBashシェルコマンドの実行
read_fileReadファイル内容の読み取り
create_fileWriteファイルの作成・書き込み
search_replaceEditファイルの編集
edit_file-diff / コードブロックによる編集
get_terminal_output-バックグラウンド端末の出力取得
delete_file-ファイルの削除
grep_codeGrepファイル内容の検索
search_fileGlobファイル名のマッチング
list_dirLSディレクトリの一覧表示
AgentTaskサブエージェントの起動(旧称 task
Skill-スキルの呼び出し
search_webWebSearchWeb 検索
fetch_contentWebFetchWeb コンテンツの取得
todo_writeTodoWriteTODO の書き込み
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ユーザーに表示されるメッセージ
continueWithPromptAgent が実行を続行するかどうか
decision"block" で汎用的なブロックを決定。権限の決定には代わりに hookSpecificOutput.permissionDecision を使用
reason決定の理由(一部のイベントは独自の reason フィールドを定義)
updatedToolOutputツールの出力を置き換える
hookSpecificOutputイベント固有フィールドのコンテナ(各イベントの説明を参照)

環境変数

Hook スクリプトの実行時に、プラグインは以下の環境変数を注入します。スクリプトから参照できます:
変数説明
QODER_SESSION_IDセッション ID
QODER_TOOL_NAME現在のツール名
QODER_CWD作業ディレクトリ
QODER_TRANSCRIPT_PATHTranscript ファイルのパス
QODER_TOOL_INPUT_FILE_PATHツールが操作するファイルパス(該当する場合)

Hooks イベント

SessionStart

セッションの開始時または再開時に発火します。セッションの開始時に起動コンテキスト(プロジェクト規約や環境情報など)を注入する用途に適しています。 matcher マッチ: マッチ対象のフィールドはありません。 追加入力フィールド:
{
  "session_id": "abc-123",
  "cwd": "/path/to/project",
  "hook_event_name": "SessionStart",
  "type": "startup",
  "model": "Auto"
}
フィールド説明
typestringセッション開始の種別。IDE は現在 "startup" を渡す
modelstringセッションのモデル設定
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.hookEventNamestring"UserPromptSubmit" 固定
hookSpecificOutput.additionalContextstring追加コンテキスト情報。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

PreToolUse

ツール実行前に発火します。ツールの実行をブロックできます。 最もよく使われるイベントで、危険コマンドの遮断やファイルパスの検証、権限チェックなどに適しています。 matcher マッチ: ツール名(BashWriteEditReadGlobGrep や、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.hookEventNamestring"PreToolUse" 固定
hookSpecificOutput.permissionDecisionstring"allow"(許可)、"deny"(拒否)、"ask"(ユーザーに確認)
hookSpecificOutput.permissionDecisionReasonstring決定理由。denyask の場合に Agent またはユーザーに表示される
hookSpecificOutput.updatedInputobject変更後のツール入力パラメータ(任意。ツールの呼び出し内容を書き換える場合に使用)
hookSpecificOutput.additionalContextstring追加コンテキスト情報(任意)

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.permissionDecisionstring"allow"(許可)、"deny"(拒否)、"ask"(ユーザーへの確認にフォールバック)
hookSpecificOutput.permissionDecisionReasonstring決定理由
hookSpecificOutput.updatedInputobject変更後のツール入力パラメータ(任意)
このイベントの出力構造は 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.hookEventNamestring"PostToolUse" 固定
hookSpecificOutput.feedbackstringフィードバック情報。ユーザーに表示される(例: 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
}
フィールド説明
errorstringツール実行のエラーメッセージ
is_interruptboolean失敗が中断によるものかどうか

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_activeboolean直前の Stop Hook のブロックにより Agent がリトライ中の場合は trueスクリプトはこのフィールドを確認し、true の場合は exit 0 して無限ループを防ぐ必要があります
last_assistant_messagestringAgent の最後のテキスト応答
Agent の停止をブロック: exit 2。ブロック理由はユーザーメッセージとして会話に注入され、Agent は作業を続行します。 stdout JSON 出力フィールド(exit 0 の場合):
{
  "decision": "block",
  "reason": "Tests failing. Fix them before completing."
}
フィールド説明
decisionstring"block"(停止をブロックし、Agent に作業を継続させる)
reasonstringブロックの理由。メッセージとして会話に注入される
無限ループの防止: 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_idstringサブエージェント ID
agent_typestringサブエージェントの種別
stop_hook_activebooleanHook のブロック後に停止がリトライされているかどうか
agent_transcript_pathstringサブエージェントの transcript ファイルのパス
last_assistant_messagestringサブエージェントの最後のテキスト応答

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 -rfgit 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)に切り出すと、チームで保守しやすくなります
  • より精度の高い検出が必要な場合は、gitleakstrufflehog などの外部ツールを呼び出してください
シナリオ 1 との違い:シナリオ 1 は exit 0additionalContext による 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 を分析するのか:UserPromptSubmitPostToolUse はその時点のやり取りしか取得できませんが、Stop の時点の Transcript ファイルにはセッションの全履歴が含まれているため、モデルの応答テキスト、ツール呼び出しの分布、成功 / 失敗率を一度に抽出できます。詳細は Transcript ファイル形式 を参照してください。

シナリオ 6:安全性の制御 — 危険コマンドの遮断

課題: Agent が rm -rfgit 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 2decision:"block"Agent の完了をブロックし、振り返りを強制できます
  • pending-review.jsonl にセッションを記録しておき、後でまとめてレビューできます
  • /retro Skill(カスタムの振り返り Skill)と組み合わせることで、自動検出 → リマインド → 蓄積のクローズドループを構成できます
現時点での実装方法:
Hooks は現在 commandhttp タイプのハンドラのみをサポートしているため(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 オブジェクトで、時系列順に追記され、セッションのやり取りの全体が記録されます。

各行の共通フィールド

フィールド説明
typestringレコード種別: session_meta / user / assistant / progress
sessionIdstringセッション ID
uuidstringこのレコードの一意な ID
timestampstringISO 8601 形式のタイムスタンプ
cwdstring現在の作業ディレクトリ
messageobjectメッセージの内容(user / assistant 種別に存在)
dataobjectメタデータ(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.contenttool_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_filesearch_replacerun_in_terminalSkill
  • input: ツール呼び出しのパラメータ
  • id: 後続の tool_resulttool_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: その他のデバッグ方法

  1. Transcript を確認する: ~/.qoder/projects/<encoded-path>/transcript/<session>.jsonljq で 1 行ずつ解析する
  2. Hook のログを確認する: Qoder IDE のログで [hook] プレフィックスを検索し、実行結果と所要時間を確認する
  3. 小さく始める: まず exit 0 だけのスクリプトで発火を確認し、その後で業務ロジックを段階的に追加する
  4. 後片付け: デバッグが終わったらデバッグコード(/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"
          }
        ]
      }
    ]
  }
}