SpyBara
Go Premium

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

This page contains 92 additions and 65 deletions.

2026
Thu 1 23:59 Fri 2 19:58

イベントに 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 を呼び出します。イベント自体は不変です:すべての深さで凍結されており、フィールドに割り当てるとスローされます。このフックは各プロンプトが送信される前にトリミングします:

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.*' はすべての設定フックイベントと一致します。'*' はテレメトリイベントを除くすべてのイベントと一致します。テレメトリイベントには、それぞれの名前と { to: 'collector' } フィルタを指定します。

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

Claude の動作をフックする

これらのイベントを処理すると、ツール呼び出し、プロンプト、またはターンが発生した時点でそれを確認したり変更したりできます。すべてのイベントとフックが返せる値については、イベントリファレンスを参照してください。

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

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

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

// The matcher limits the hook to Bash calls, so e.command is the shell command
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
  if (/git push .*--force/.test(e.command)) {
    // Returning without calling next answers the event, so the command never runs
    return { deny: 'Force pushes are not allowed in this repository. Push to a new branch instead.' }
  }
  // Every other command goes on to the permission check and then to Bash
  return next(e)
})

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

ツールの実行後に処理を行うには、await next(e) を実行してから処理を行い、next が返したものを返します。このフックは、Claude が変更した各 .mdx ファイルを $.ui.log で記録します。これはトランスクリプトに薄い色の行を追加するもので、Claude はその行を読みません:

on('tool.call', { tool: ['Edit', 'Write'] }, async ($, e, next) => {
  // Wait for the permission check and the tool, and keep what they produced
  const result = await next(e)
  // A refused call comes back as { deny }, and a failed one has isError set
  const changed = !result.deny && !result.isError
  if (changed && e.file_path.endsWith('.mdx')) $.ui.log('Claude changed ' + e.file_path)
  // Return the result as it came, so Claude reads what the tool returned
  return result
})

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

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

組織の管理設定にあるフックは、どの mod の tool.call フックよりも先に実行され、それらによるブロックは最終的なものです。

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

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

この例の RISKY パターンは rm -r、rm -rf、git reset --hard、および --force 付きの git push に一致し、git push -f などの別の書き方には一致しません。このモジュールは、このパターンに一致する 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) => {
    // Let every other command through without a question
    if (!RISKY.test(e.command)) return next(e)
    // Start from the safe answer, so a question nobody answers refuses the command
    let answer = 'Refuse'
    try {
      // The tool call waits here until the user picks one of the two labels
      answer = await $.ui.ask('Run this command? ' + e.command, ['Run it', 'Refuse'])
    } catch {
      // The user dismissed the question, or this is a claude -p run with nobody to ask
    }
    if (answer !== 'Run it') {
      // Answer without calling next, so the command doesn't run
      return { deny: 'The user declined this command. Ask before trying a different approach.' }
    }
    return next(e)
  })
}

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

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

待機は $.ui.ask などの mods API 呼び出しの中で行ってください。その時間はフックの時間制限に含まれないためです。自分で作成した promise を待つ時間は含まれます。Claude Code はタイムアウトしたフックをスキップするため、保留していたコマンドが実行されてしまいます。

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

ツール呼び出しを実行してよいかどうかを決定するには、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) => {
  // What the permission rules and settings hooks decided: 'allow', 'ask', or '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 ホストでブランチを保護してください。

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

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

prompt.submit フックはターンが始まる前に各プロンプトを確認できるため、テキストを書き換えたり、追加したりできます。e.text は入力された内容です。

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

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

on('prompt.submit', async ($, e, next) => {
  // Pass on a prompt that doesn't mention a pull request as it is
  if (!/\bPR\b|pull request/i.test(e.text)) return next(e)
  const git = await $.process.run(['git', 'branch', '--show-current'])
  // Outside a git repository the command fails, so there's no branch to add
  if (git.exitCode !== 0) return next(e)
  // Keep any context an earlier hook added, and add one more line for Claude
  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 を使用します。これらのフックからのテキストがリクエストごとに変わると、プロンプトキャッシュが無効化されます。

ターンを追跡する

ターンとは、1 つのプロンプトに応えて Claude が行うすべての処理です。ターンを追跡するには、turn.start、turn.step、turn.complete を処理します:

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

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

// function* makes the hook a generator, which can pass the response on piece by piece
on('turn.step', async function* ($, e, next) {
  // Send the request, forward each piece as it arrives, and keep the finished result
  const result = yield* next(e)
  // Skip a result that reports no token counts
  if (result.usage) {
    $.ui.log('cache read ' + result.usage.cache_read_input_tokens + ' · wrote ' + result.usage.cache_creation_input_tokens)
  }
  // Return the result unchanged, so the turn continues as usual
  return result
})

Claude の応答は mod がない場合と同じように画面にストリーミングされます。各リクエストが完了すると、トランスクリプトの薄い色の行に、キャッシュから読み取られたトークン数とキャッシュに書き込まれたトークン数が表示されます。ツール呼び出しを含むターンには複数のリクエストがあるため、複数の行が追加されます。

result.usage は、Claude API がリクエストについて報告するトークン数(input_tokens、output_tokens、cache_read_input_tokens、cache_creation_input_tokens)と、応答した model を保持します。フックはサブエージェントのリクエストでも実行されるため、メインの会話だけを対象にしたい場合は e.agentId を確認してください。

設定フックのイベントを処理する

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

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

on('classic.Stop', async ($, e, next) => {
  // e has the same fields a Stop hook in a settings file reads from stdin
  $.ui.log('Transcript saved at ' + e.transcript_path)
  // Pass the event on, so Stop hooks in your settings files still run
  return next(e)
})

Claude が応答を終えるたびに、トランスクリプトの薄い色の行にトランスクリプトファイルのパスが表示されます。フックは next(e) を返すため、イベントを観察するだけで、ターンの終わり方は何も変わりません。

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

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

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 をスキップしてコマンドを実行します。ハンドラには、より短い独自の制限時間があります。

次のステップ