SpyBara
Go Premium

plugins/mods/events.md 2026-10-01 23:59 UTC to 2026-10-02 06:02 UTC

This page contains 28 additions and 1 deletion.

2026
Thu 1 23:59 Fri 2 07:00

イベントに mod で反応する

mod から Claude Code イベントを処理する:観察、書き換え、またはツール呼び出し、プロンプト、ターンに答える、フック が処理するイベントをフィルタリングする、および他の mod を計画する。

hook はイベントハンドラです。Claude Code が名前付きイベントが発生したときに実行する関数です。Claude Code は、ツールを実行する、プロンプトを送信する、モデルにリクエストを送信する、またはセッションを開始または終了するなど、アクションを起こそうとしている各ポイントでイベントを発火させます。hook は Claude Code がアクションを起こす前に実行されるため、イベントを観察したり、書き換えたり、Claude Code の代わりに答えたりできます。hook は on(eventName, handler) で登録します。

ここから始める前に、最初の mod を構築してください。すべてのイベントとその正確なフィールドについては、リファレンスを参照するか、ビルド用の型を読んでください。

hook がイベントを処理する方法

hook はイベントと Claude Code がそれについて何をするかの間に位置するため、イベントを観察したり、書き換えたり、それ自体で答えたりできます。3 つの引数を受け取ります:mods API を $ として、イベントを e として、次のハンドラを next として。イベントのハンドラはミドルウェアチェーンを形成します。next(e) は次のハンドラを呼び出します。これは別の mod の hook か、チェーンの最後では Claude Code 独自の動作であり、結果に解決されます。hook が next で何をするかが、3 つのうちどれをするかを決定します。

イベントを観察する

イベントを変更せずに観察するには、作業を行い、next(e) を返します。この hook は Claude が使用しようとしている各ツールをログに記録します:

on('tool.call', async ($, e, next) => {
  // ツールが実行される前に実行される
  $.ui.log('Claude is about to use ' + e.tool)
  // イベントを変更せずに渡す
  return next(e)
})

各ツールが実行される前に、● my-mod: Claude is about to use Bash のような薄い行がトランスクリプトに表示されます。ここで my-mod はプラグインの名前です。ツールは mod なしで実行されるのと同じように実行されます。

イベント後にアクションを実行するには、await next(e) を実行し、作業を行い、結果を返します。この hook は各ツールが実行された後にログに記録します:

on('tool.call', async ($, e, next) => {
  // ツールを実行し、その結果を待つ
  const result = await next(e)
  // ツールが実行された後に実行される
  $.ui.log(e.tool + ' finished')
  // 結果を変更せずに返す
  return result
})

行は各ツールが完了した後に表示されるようになります。hook が next(e) が解決したものを返すため、Claude は同じ結果を読みます。

イベントを書き換える

Claude Code が作用する内容(プロンプトのテキストなど)を変更するには、イベントの変更されたコピーで next を呼び出します。イベント自体は不変です:すべての深さで凍結されており、フィールドに割り当てるとスローされます。この hook は各プロンプトが送信される前にトリミングします:

on('prompt.submit', async ($, e, next) => {
  // イベントのコピーをテキストが変更された状態で渡す
  return next({ ...e, text: e.text.trim() })
})

後のハンドラと Claude Code は、トリミングされたプロンプトを受け取り、元のプロンプトを見ることはありません。結果を変更することもできます:await next(e) を実行してから、フィールドが置き換えられた結果のコピーを返します。

イベントに答える

イベント自体を処理するには、next を呼び出さずに結果を返します。これはチェーンをショートサーキットするため、後の mod と Claude Code 独自の動作は実行されません。この hook はすべての Bash コマンドを拒否します:

on('tool.call', { tool: 'Bash' }, async () => {
  // next への呼び出しがないため、コマンドは実行されない
  return { deny: 'Bash is turned off in this project. Use the file tools.' }
})

Claude が Bash コマンドを試みると、コマンドは実行されず、Claude は deny テキストをツールの結果として読みます。各イベントには独自の結果形状があり、イベントリファレンスがリストアップしています。

hook が処理するイベントをフィルタリングする

hook を一部のイベントのみで実行するには、on の 2 番目の引数としてフィルタを渡します。Claude Code はフィルタを matcher と呼びます。これはイベントのフィールドと比較されるフィールドを持つオブジェクトであり、hook はすべてのフィールドが一致する場合にのみ実行されます。フィールドは値、許可された値の配列、または正規表現です。

この例の各行は、同じ関数 hook をより狭いツール呼び出しセットに登録します:

// 文字列は 1 つの値と一致する:Bash 呼び出しのみ
on('tool.call', { tool: 'Bash' }, hook)
// 配列は任意の値と一致する:Edit 呼び出しと Write 呼び出し
on('tool.call', { tool: ['Edit', 'Write'] }, hook)
// 正規表現はパターンで一致する:1 つの MCP サーバーのすべてのツール
on('tool.call', { tool: /^mcp__github__/ }, hook)

hook は Bash、Edit、または Write 呼び出しで 1 回実行され、名前が mcp__github__ で始まるツールへの呼び出しで 1 回実行されます。Read などの他のツールへの呼び出しは 3 つのいずれにも一致しないため、hook はそれに対して実行されません。

イベント名はワイルドカードです。'classic.*' はすべての設定 hook イベントと一致します。'*' はテレメトリイベントを除くすべてのイベントと一致します。テレメトリイベントは名前で、または 'telemetry.*' として hook します。

各イベントを matcher ごとに 1 回登録します。matcher なしで session.start に対して on を 2 回呼び出すと、モジュールは on("session.start") is registered twice without a matcher で読み込みに失敗します。mod がセッション開始時に行うすべてのことを 1 つの hook に入れます。

Claude が何をしているかを hook する

これらのイベントを hook して、ツール呼び出し、プロンプト、またはターンが発生しているのを見たり、変更したりします。すべてのイベントと hook が返すことができるものについては、イベントリファレンスを参照してください。

ツール呼び出しをガードまたは変更する

tool.call hook は Claude が使用しようとしている各ツールを見るため、呼び出しを拒否したり、その引数を変更したり、それを通したりできます。tool.call は Claude Code がツールを実行しようとしているときに発火します。これには subagent が行う呼び出しと MCP ツールへの呼び出しが含まれます。e.tool はツールの名前であり、ツールの引数は e のフィールドです。例えば Bash の場合は e.command です。next(e) を呼び出すと、Claude Code は権限チェックを実行してからツールを実行します。

この hook は force-push を行う Bash コマンドを拒否し、Claude に理由を伝えます:

// matcher は hook を Bash 呼び出しに制限するため、e.command はシェルコマンド
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
  if (/git push .*--force/.test(e.command)) {
    // next を呼び出さずに返すことでイベントに答えるため、コマンドは実行されない
    return { deny: 'Force pushes are not allowed in this repository. Push to a new branch instead.' }
  }
  // 他のすべてのコマンドは権限チェックを経由して Bash に進む
  return next(e)
})

Claude が git push --force を試みると、コマンドは実行されず、hook が next を呼び出さないため権限プロンプトは表示されません。Claude は deny テキストをツールの結果として読むため、Claude が作用できる指示として書いてください。他のすべての Bash コマンドは mod なしで実行されるのと同じように実行されます。

ツールが実行された後にアクションを実行するには、await next(e) を実行し、作業を行い、next が与えたものを返します。この hook は Claude が変更する各 .mdx ファイルをログに記録します。$.ui.log を使用します。これはトランスクリプトに薄い行を追加し、Claude は読みません:

on('tool.call', { tool: ['Edit', 'Write'] }, async ($, e, next) => {
  // 権限チェックとツールを待ち、それらが生成したものを保持する
  const result = await next(e)
  // 拒否された呼び出しは { deny } として戻り、失敗したものは isError が設定されている
  const changed = !result.deny && !result.isError
  if (changed && e.file_path.endsWith('.mdx')) $.ui.log('Claude changed ' + e.file_path)
  // ツールが返したものを Claude が読むように、結果をそのまま返す
  return result
})

Claude が .mdx ファイルを編集または書き込んだ後、トランスクリプトの薄い行がファイルに名前を付けます。別の種類のファイルの場合、または拒否または失敗した呼び出しの場合は何もログに記録されません。hook が受け取った結果を返すため、Claude の呼び出しの見方は変わりません。

呼び出しを変更するには、変更された引数を next に渡します。呼び出しを再試行するには、next(e) を再度呼び出します:最初の結果で isError を見る hook は、ツールを 2 番目の時間実行して、その結果を返すことができます。呼び出しに自分で答えるには、next を呼び出さずに result フィールドを持つオブジェクト(例えば { result: 'Skipped by my-mod' } など)を返します。そうすると、権限プロンプトは表示されず、ツールは実行されないため、返す結果は Claude が何が起こったかについて学ぶすべてです。

組織の管理設定の hook は、任意の mod の tool.call hook の前に実行され、そのうちの 1 つからのブロックは最終的です。

ユーザーが決定するまでツール呼び出しを保持する

hook はツール呼び出しを一時停止し、先に進む前にユーザーに何をするかを尋ねることができます。tool.call hook は next を呼び出す前または返す前に await でき、ツール呼び出しはそれまで保留されたままです。質問をユーザーに提示するには、$.ui.ask を呼び出します。これはあなたの質問を Claude があなたに何かを尋ねるために使用するダイアログで、番号付きのオプションリストの上に表示し、ユーザーが選んだラベルに解決されます。オプションの後、ダイアログは異なる答えを入力するための行と Chat about this 行を追加します。

この例の RISKY パターンは rm -r、rm -rf、git reset --hard、および --force を含む git push と一致し、git push -f などの他のスペルを見落とします。このモジュールは RISKY パターンと一致する Bash コマンドを実行する前に尋ねます:

const RISKY = /\brm\s+-rf?\b|\bgit\s+reset\s+--hard\b|\bgit\s+push\b.*--force/

export function register(on) {
  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
    // 質問なしで他のすべてのコマンドを通す
    if (!RISKY.test(e.command)) return next(e)
    // 安全な答えから始めるため、誰も答えない質問はコマンドを拒否する
    let answer = 'Refuse'
    try {
      // ツール呼び出しはここで待機し、ユーザーが 2 つのラベルのいずれかを選ぶまで
      answer = await $.ui.ask('Run this command? ' + e.command, ['Run it', 'Refuse'])
    } catch {
      // ユーザーが質問を却下したか、これは誰も尋ねる人がいない claude -p 実行
    }
    if (answer !== 'Run it') {
      // next を呼び出さずに答えるため、コマンドは実行されない
      return { deny: 'The user declined this command. Ask before trying a different approach.' }
    }
    return next(e)
  })
}

Claude が rm -rf build などのコマンドを試みると、質問がコマンドと共に表示され、コマンドは答えを待ちます:

  • ユーザーが Run it を選ぶ:hook は next(e) を呼び出し、通常の権限チェックはその後も実行されます
  • ユーザーが Refuse を選ぶ:コマンドは実行されず、Claude は deny テキストを読みます
  • ユーザーが答えを入力する:$.ui.ask は入力されたテキストに解決されます。hook はそれを Run it と比較するため、他のテキストはコマンドを拒否します。
  • 誰も答えない:ユーザーが質問を却下するか Chat about this を選ぶと、または claude -p 実行では $.ui.ask が拒否されるため、catch ブロックは答えを Refuse のままにします

$.ui.ask などの mods API 呼び出しの中で待機を保持してください。その時間は hook の10 秒の時間制限に対してカウントされないためです。自分の promise を待つのに費やされた時間はカウントされます。Claude Code は時間切れになった hook をスキップするため、保持されたコマンドは実行されます。

ユーザーに確認される前にツール呼び出しを承認または拒否する

ツール呼び出しを実行してよいかどうかを決定するには、Claude Code がその決定を行うイベントである tool.check を処理します。これは権限ルールと設定フックが決定した後に発火し、next(e) はそれらの決定(allow、ask、または deny)に解決されます。フックはその決定または別の決定を返します。e.input はツールの引数を保持します。例えば Bash の場合は command です。

固定のコマンドやパスには、Bash(npm test) のような権限ルールを使用してください。これにはコードは不要です。現在の Git ブランチや別のフックが記録した値など、その時点の状態に決定が依存する場合に tool.check を処理します。

このフックは、現在のブランチが main の間 git push を拒否します:

on('tool.check', { tool: 'Bash' }, async ($, e, next) => {
  // 権限ルールと設定フックが決定したもの:'allow'、'ask'、または 'deny'
  const decided = await next(e)
  if (!e.input.command.includes('git push')) return decided
  const branch = await $.process.run(['git', 'branch', '--show-current'])
  if (branch.stdout.trim() !== 'main') return decided
  return { decision: 'deny', reason: 'Push from a branch other than main' }
})

main では、ルールが git push を許可している場合でも、フックは deny を返します。別のブランチの場合や他のコマンドの場合、呼び出しは mod がない場合と同じ決定を受けます。

フックはコマンドのテキストと照合するため、Claude へのリマインダーとして扱ってください。すべてのユーザーに対して main へのプッシュをブロックするには、Git ホストでブランチを保護してください。

フックは 3 つの決定のいずれも返せるため、管理設定外の PreToolUse フックがブロックした呼び出しを承認することもできます。どの決定が mod よりも優先されるかについては、フックで権限を拡張するを参照してください。

プロンプトを書き換えるまたは追加する

prompt.submit hook は各プロンプトをターンが開始される前に見るため、テキストを書き換えたり、それに追加したりできます。e.text は入力されたものです。

これを行うには これを返す
プロンプトを書き換える。トランスクリプトのメッセージは新しいテキストを表示する。 next({ ...e, text: newText })
Claude のみが読むテキストを追加し、プロンプトの後 next({ ...e, context: [...(e.context ?? []), extraText] })
プロンプトが送信されるのを停止する { drop: 'the reason' }

この hook は、プロンプトがプルリクエストに言及するたびに、Claude に現在のブランチ名を追加します:

on('prompt.submit', async ($, e, next) => {
  // プルリクエストに言及しないプロンプトをそのまま渡す
  if (!/\bPR\b|pull request/i.test(e.text)) return next(e)
  const git = await $.process.run(['git', 'branch', '--show-current'])
  // git リポジトリの外ではコマンドが失敗するため、追加するブランチはない
  if (git.exitCode !== 0) return next(e)
  // 前の hook が追加したコンテキストを保持し、Claude 用にもう 1 行追加する
  return next({ ...e, context: [...(e.context ?? []), 'Current branch: ' + git.stdout.trim()] })
})

open a PR for this change などのプロンプトを送信すると、メッセージはトランスクリプトで同じに見え、Claude は Current branch: feature/auth などの行もそれの後に読みます。プルリクエストに言及しないプロンプトは変更されずに通過し、git は実行されません。

他のイベントは Claude が読むもののその他をカバーします:システムプロンプトの各セクションの prompt.section、最初のメッセージで送信されるコンテキストの prompt.context、スキルのテキストの skill.prompt。これらの hook からのテキストがリクエスト間で変更される場合、プロンプトキャッシュが無効化されます。

ターンをフォローする

ターンは 1 つのプロンプトに答えて Claude が行うすべてです。turn.start、turn.step、および turn.complete を hook してフォローします:

イベント いつ発火するか hook が何をできるか
turn.start ターンが開始される 観察。e.turnId は他の 2 つのイベントでターンを識別します。
turn.step Claude Code がモデルに 1 つのリクエストを送信しようとしている。ツール呼び出しを持つターンは複数あります。e.agentId は subagent のリクエストに対して設定されます。 各リクエストのトークン使用量を読み、next({ ...e, model }) で別のモデルに送信するか、モデルを呼び出さずに答える
turn.complete ターンが終了した。ユーザーが中断したターンを含む。e.isAborted は true です。e.answer は Claude の最終テキスト、e.durationMs はそれにかかった時間、e.usage はターンのトークン合計です。subagent のターンは e.agentId が設定された状態で発火します。 観察するか、{ text: 'Done in 12 seconds' } などの text フィールドを持つオブジェクトを返して、答えの下に行を表示する

turn.step hook を非同期ジェネレータとして書いてください。イベントがストリーミングされるためです。yield* next(e) はレスポンスをストリーミングされるときに転送し、完成した結果に評価されます。この hook は各リクエストの Claude API がプロンプトキャッシュから提供した量をログに記録します:

// function* は hook をジェネレータにし、レスポンスを部分的に渡すことができる
on('turn.step', async function* ($, e, next) {
  // リクエストを送信し、到着時に各部分を転送し、完成した結果を保持する
  const result = yield* next(e)
  // トークン数を報告しない結果をスキップする
  if (result.usage) {
    $.ui.log('cache read ' + result.usage.cache_read_input_tokens + ' · wrote ' + result.usage.cache_creation_input_tokens)
  }
  // 結果を変更せずに返すため、ターンは通常通り続行される
  return result
})

Claude のレスポンスは mod なしで画面にストリーミングされます。各リクエストが完了した後、トランスクリプトの薄い行がキャッシュから読み取られたトークン数と書き込まれたトークン数を示します。ツール呼び出しを持つターンは複数のリクエストを持つため、複数の行を追加します。

result.usage は Claude API がリクエストに対して報告する 4 つのトークン数を保持します。さらに答えた model:input_tokens、output_tokens、cache_read_input_tokens、および cache_creation_input_tokens。hook は subagent のリクエストに対しても実行されるため、メインの会話のみが必要な場合は e.agentId をチェックしてください。

設定 hook イベントを hook する

設定 hook は、設定ファイルで構成するコマンド、HTTP、プロンプト、およびエージェント hook です。各設定 hook イベント(Stop、SessionEnd、PostToolUse など)は、classic. の後に設定 hook イベントの名前が続く classic.Stop などのイベントでもあります。e は設定 hook が stdin で受け取る JSON であり、transcript_path を含みます。

この hook は Claude が応答を終了したときに発火する Stop を使用して、セッションのトランスクリプトが保存されている場所をログに記録します:

on('classic.Stop', async ($, e, next) => {
  // e は設定ファイルの Stop hook が stdin から読む同じフィールドを持つ
  $.ui.log('Transcript saved at ' + e.transcript_path)
  // イベントを渡すため、設定ファイルの Stop hook はまだ実行される
  return next(e)
})

Claude が応答を終了するたびに、トランスクリプトの薄い行がトランスクリプトファイルのパスを示します。hook は next(e) を返すため、イベントを観察し、ターンの終了方法について何も変更しません。

他の mod と並行して実行する

複数の mod が同じイベントを hook でき、そのいずれかが失敗する可能性があります。mod がツール呼び出しをブロックする場合、チェーン内のその位置と hook が失敗したときに何が起こるかを確認してください。

mod が実行される順序

同じイベントの hook は 1 つのミドルウェアチェーンを形成します。各 mod の next は次の mod の hook を呼び出し、最後の next は Claude Code 独自の動作に到達します。最初の mod は最も外側です:他の前にイベントを見て、その後に結果を見て、他が実行されるかどうかを決定します。後の mod は前の mod がイベントを見るのを止めることはできません。

Claude Code は各 mod がどこから来るかによってチェーンを順序付けます:

  1. 組み込みガード sec-default@builtin。Claude Code に組み込まれた mod。/plugin は cc-plugin-sec-default としてリストアップします。ここで読み込まれます。組織が prependPlugins にリストアップする mod。その後、組織に属し、appendPlugins にない他の mod
  2. インストールする mod
  3. 組織が appendPlugins にリストアップする mod
  4. Claude Code に組み込まれた他の mod

インストールする mod の中で、mod はマニフェストの dependencies の下にリストアップする mod の前に実行されます。1 つのモジュール内で、hook は register が on を呼び出した順序で実行されます。

設定 hook が順序で実行される場所

設定ファイルで構成された PreToolUse hook はツール呼び出し中にも実行され、mod のチェーンの固定ポイントで実行されます:

  • 管理設定からの PreToolUse hook:最初の mod の tool.call hook の前に実行され、そのうちの 1 つからのブロックは最終的であるため、mod はその呼び出しを見ません。
  • 他のすべての設定ファイルおよびプラグインの hooks/hooks.json からの PreToolUse hook:最後の mod が next を呼び出した後、Claude Code 独自の動作の一部として実行されます。tool.call に答える mod が next を呼び出さずに、それらの実行を保持し、next を呼び出す mod はそれらの決定を返される結果で見ます。

tool.check はこれらのフックと権限ルールが決定した後に発火するため、そのフックは 2 番目のグループのフックがブロックした呼び出しを承認できます。

失敗した hook を処理する

失敗した hook はセッションを破壊しません。代わりに何が起こるかを決定できます。.catch ハンドラのない hook がスロー、タイムアウト、または間違った形の結果を返すと、次に何が起こるかは next を呼び出したかどうかに依存します:

  • next を呼び出す前に失敗した:Claude Code はそれをスキップし、次のハンドラがその代わりに実行されます
  • next が解決した後に失敗した:その結果は成立し、何も 2 番目の時間実行されません

1 行は mod、イベント、および理由に名前を付けます。例えば my-mod: tool.call hook skipped: threw Error: boom。それを読む場所はセッションに依存します。mod が何もしない理由を見つけ出すがリストアップしています。描画が検証されない ui.render hook は異なる方法で報告されます。要素からツリーを構築するが説明しています。

呼び出しをブロックする hook を失敗クローズにするには、その代わりに答える .catch エラーハンドラを追加します。ここで、guard は hook 関数です:

// on は登録を返し、.catch はその 1 つの hook にハンドラを接続する
on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
  // next.error.kind は 'throw' または 'timeout' であり、guard がどのように失敗したかを示す
  return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
})

guard が機能している間、ハンドラは実行されません。guard が Bash 呼び出しでスロー、またはタイムアウトすると、Claude Code は同じイベントでハンドラを呼び出します。ハンドラは { deny } を返すため、コマンドは実行されず、Claude はテキストを throw または timeout で終わるのを読みます。ハンドラなしでは、Claude Code は guard をスキップしてコマンドを実行します。ハンドラは1 秒で答える必要があります。

次のステップ