SpyBara
Go Premium

plugins/mods/events.md 2026-11-12 23:02 UTC to 2026-11-13 02:58 UTC

No changes for this document in this history range.

2026
Mon 9 23:02 Tue 10 21:57

イベントに 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 がない場合と同じように実行されます。

このフックが例外をスローするかタイムアウトした場合、Claude Code はフックをスキップし、コマンドは mod がない場合と同じように処理されます。代わりにコマンドを拒否するには、フックに .catch ハンドラーを追加してください。

ツールの実行後に処理を行うには、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 に与えてください。権限プロンプトは表示されずツールも実行されないため、返した結果が、何が起きたかについて Claude が知るすべてになります。
  • 結果を Claude に渡さない:await next(e) の後に { deny: reason } を返します。Claude は next が返したものの代わりに、指定した理由を読みます。ツールが実行された場合、deny はその結果を Claude に渡さないようにするだけで、ツールが行った処理は何も元に戻しません。ツールが実行されて成功した場合、理由は Bash ran, and a plugin withheld its result: のような注記の後に続きます。

組織の管理設定にあるフックは、どの mod の tool.call フックよりも先に実行されます。管理設定のフックが呼び出しをブロックした場合、その呼び出しに対してはどの 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 はタイムアウトしたフックをスキップするため、保留していたコマンドが実行されてしまいます。代わりにコマンドを拒否するには、on が返す登録に .catch ハンドラーをチェーンしてください。このハンドラーは、フック内部の catch ブロックとは別のものです。

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

ツール呼び出しを実行してよいかどうかを決定するには、Claude Code がその決定を行うイベントである tool.check を処理します。

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

  • 返す値:自分で呼び出しを決定するには、{ decision: 'allow' } や { decision: 'deny', reason } のような独自のオブジェクトを返します。決定を変更しない場合は、await next(e) が返すものを返します。
  • await next(e) が返すもの:Claude Code 自身の決定で、decision が allow、ask、deny のいずれかであるオブジェクトです。next(e) の実行中に権限プロンプトは表示されません。
  • 引数:e.input はツールの引数を保持します。例えば Bash の場合は command です
  • tool.check が発火するタイミング:PreToolUse フックが実行された後、かつ権限プロンプト、auto モードの分類器、または PermissionRequest フックより前

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

on('tool.check', { tool: 'Bash' }, async ($, e, next) => {
  // Claude Code's own decision
  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 を拒否し、それ以外の呼び出しはすべてそのまま通します:

  • main の場合:ルールが git push を許可している場合でも、フックは deny を返します
  • 別のブランチの場合、および他のコマンドの場合:呼び出しは mod がない場合と同じ決定を受けます
  • フックが例外をスローするかタイムアウトした場合:この場合も、呼び出しは mod がない場合と同じ決定を受けます。代わりに呼び出しを拒否するには、{ decision: 'deny', reason: 'the reason' } を返す .catch ハンドラーを追加してください。

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

[!NOTE]

tool.check フックは allow や ask を返すこともできます。plan モードでは、ファイル編集など読み取り専用ではない呼び出しについて、フックは next(e) が返す決定を厳しくすることはできますが、緩めることはできません。フックの allow で置き換えられない権限ルールと PreToolUse フックの決定については、フックで権限を拡張するを参照してください。

ツール呼び出しが読み取り専用かどうかを確認する

tool.check フックでは、ツールが呼び出しを読み取り専用として報告した場合に e.isReadOnly が true になります。それ以外の場合、このフィールドは false ではなく存在しません。e.isReadOnly には Claude Code v2.1.304 以降が必要です。

Read、Grep、Glob、WebSearch などの一部のツールは、すべての呼び出しを読み取り専用として報告します。Bash と MCP ツールは、次の場合に呼び出しを読み取り専用として報告します:

  • Bash:コマンドが Claude Code の組み込みの読み取り専用コマンドチェックに合格した場合
  • MCP ツール:サーバーがツールの readOnlyHint アノテーションを true に設定している場合

この値はツール自身の報告であるため、このフィールドだけに基づいて呼び出しを承認しないでください。例えば、WebFetch と Agent の呼び出しは読み取り専用として報告されますが、WebFetch は URL にリクエストを送信し、Agent は独自のツールを実行するサブエージェントを開始します。

このフックは、読み取り専用ではなく、まだ拒否されていないすべての mcp__github__ 呼び出しに対して ask を返します:

on('tool.check', { tool: /^mcp__github__/ }, async ($, e, next) => {
  const decided = await next(e)
  // Read-only calls and denied calls pass through unchanged
  if (e.isReadOnly || decided.decision === 'deny') return decided
  return { decision: 'ask', reason: 'This tool can change the GitHub repository' }
})

呼び出しが読み取り専用かどうかで tool.check フックを絞り込む

e.isReadOnly によってフックが処理する呼び出しを絞り込むには、読み取り専用の呼び出しには matcher を、それ以外にはフック本体を使用します:

  • 読み取り専用の呼び出しのみ:on('tool.check', { tool: 'Bash', isReadOnly: true }, hook) のように、matcher に isReadOnly: true を追加します
  • 読み取り専用ではない呼び出し:mcp__github__ の例のように、フック本体で e.isReadOnly を判定します。matcher に isReadOnly: false を指定すると、フックは実行されません。

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

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 は実行されません。

プロンプトを止めるには、next を呼び出さずに { drop: 'the reason' } を返します。テキストはユーザーのプロンプト入力欄に戻り、ユーザーには Prompt dropped by a hook: に続けて理由が表示されるため、理由はユーザーに向けて書いてください。次の場合、プロンプトはそのまま送信されます:

  • フックの next(e) 呼び出しでプロンプトを通過させた後に drop を返した場合:ターンはそのまま実行され、フックは a drop after its next() was answered を含むメッセージとともに失敗します
  • フックが例外をスローするかタイムアウトした場合:Claude Code はフックをスキップしてプロンプトを送信します。フックが止めるはずだったプロンプトが失敗によって送信されないよう、.catch ハンドラーを追加してください。

Claude が読むその他の内容は他のイベントで扱います。システムプロンプトの各セクションには prompt.section、最初のメッセージとともに送信されるコンテキストには prompt.context、スキルのテキストには skill.prompt を使用します。会話の開始後にセクションを変更するには、変更したシステムプロンプトセクションまたはツールの説明を送信するを参照してください。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 を確認してください。

リクエスト中に API 自身が実行したツール呼び出し(advisor ツールへの呼び出しなど)を確認するには、result.serverToolUses を読み取ります。Claude Code はこれらの呼び出しを実行しないため、これらに対して tool.call フックや tool.check フックは発火しません。レスポンスにそのような呼び出しが含まれない場合、このフィールドは存在しません。また、このフィールドには Claude Code v2.1.290 以降が必要です。

リクエストを別のモデルに送信する

model が haiku などのエイリアスのときに turn.step のリクエストがモデルエラーで失敗する場合は、Claude Code v2.1.309 以降に更新するか、完全なモデル ID を使用してください。v2.1.309 より前では、エイリアスは書かれたとおりに送信されていました。

リクエストを別のモデルに送信するには、yield* next({ ...e, model: 'haiku' }) のように、model を変更したイベントを next に渡します。

設定の読み取りを変更または拒否する

settings.read フックを使用すると、mod による読み取りと Claude Code 自身による読み取りのどちらについても、設定の読み取りを変更または拒否できます:

  • mod による読み取り:mod が $.settings.read を呼び出します。
  • Claude Code 自身による読み取り:フックは、セッションの mod が読み込まれた後、Claude Code がセッションの設定をマージするときに初めて実行されます。設定または読み込まれたプラグインが変わると、フックは再度実行されます。

どの mod がこのイベントを受け取るかと、Claude Code のバージョンを確認してください:

  • 組み込みガード:組み込みガードが読み込まれる環境では、インストールした mod や --plugin-dir で読み込んだ mod のこのフックを組み込みガードがスキップすることがあります。確認するには、デバッグログで settings.read bypassed by cc-plugin-sec-default を検索してください。そのテキストを含む行に自分の mod の名前がある場合は、組織の管理者にその mod を組織の mod の 1 つとしてインストールするよう依頼してください。
  • バージョン:Claude Code 自身による読み取りでこのイベントが発火すること、および e.layers フィールドには、Claude Code v2.1.304 以降が必要です。

settings.read フックが受け取るもの

フィールド 型 保持する内容
e.layers array 設定を保持する使用中の各ソースごとに 1 つの { source, settings } オブジェクト。優先順位が最も低いものから順に並びます:plugin、user、project、local、flag、policy。claude plugin test では空です。
e.source string $.settings.read({ source: 'policy' }) のように mod が要求した 1 つのソース。この場合、e.layers にはそのレイヤーだけが含まれます。Claude Code 自身による読み取りや、マージされた設定のその他の読み取りでは存在しません。

e.layers において、plugin は有効なプラグインが提供する設定を保持します。これはレイヤー名にすぎず、$.settings.read に渡せる source ではありません。flag は --settings で渡された設定を保持し、policy は管理設定を保持します。

Claude Code 自身による読み取りと mod による読み取りを区別する

next.origin を確認してください:

  • Claude Code 自身による読み取り:{ plugin: 'engine', tier: 'core' }
  • mod による読み取り:plugin は呼び出し元の mod の名前です

settings.read フックが返せるもの

読み取りをそのままにするには next(e) の結果を変更せずに返し、それ以外の場合は次のいずれかを返します:

戻り値 Claude Code 自身による読み取りの場合 mod による読み取りの場合
{ value } 変更される各キーについて、その値がキーのスキーマに適合する場合、セッションのマージされた設定内でそのキーの値全体を置き換えます その mod の呼び出しは value、または value から取り出された、mod が要求した key に解決されるため、配列ではなくオブジェクト全体を返してください
{ deny: reason } セッションは、いずれかのフックが応答する前にマージされた状態の設定を維持し、デバッグログに reason が記録されます その mod の呼び出しは <calling mod>: $.settings.read: <reason> で reject されます

Claude Code 自身による読み取りでは、省略したキーは未設定になるため、next(e) の value をもとに { value } を構築してください。Claude Code が env などのキーを各ソースから個別に読み取る場合、value でそのキーを変更しても効果はありません。

next(e) が settings.read フックに返すもの

next(e) は { value } または { deny } に解決されます:

  • { value }:value はマージされた設定、または e.source が設定されている場合は 1 つのソースの設定です。自分のフックより後に実行されるフックが独自の { value } を返した場合は、代わりにそのフックの value を受け取ります。
  • { deny }:自分のフックより後に実行されるフックが読み取りを拒否しました。その { deny } をそのまま返してください。

settings.read フックから 1 つの設定を変更する

まず、どの mod がこのイベントを受け取るか、またどのバージョンからかを確認してください。

1 つのキーを変更するには、このフックが spinnerTipsEnabled に対して行っているように、next(e) の value を展開してそのキーを設定します。このフックは next.origin を確認しないため、Claude Code 自身による読み取りでも mod によるマージされた設定の読み取りでも同様にキーを変更します:

on('settings.read', async ($, e, next) => {
  const read = await next(e)
  // Pass through a read of one source, so that only the merged settings change.
  // Pass through a { deny } from a later hook too: it has no value.
  if (e.source || !read.value) return read
  // Keep every other key and change this one
  return { value: { ...read.value, spinnerTipsEnabled: false } }
})

Claude Code 自身による読み取りで、フックが文字列などキーのスキーマ外の値を返した場合、そのキーだけは、いずれかのフックが応答する前にマージされた値を維持します。その場合、デバッグログには the answer for <key> is no value the key can hold と記録されます。値がスキーマに適合しているのにキーが変わらない場合は、キーが反映されない理由を参照してください。

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

設定フックとは、設定ファイルで構成するコマンド、HTTP、プロンプト、エージェントの各フックです。各設定フックイベント(Stop、SessionEnd、PostToolUse など)は、classic. の後に設定フックイベントの名前が続くイベント(classic.Stop など)としても利用できます。e は設定フックが stdin で受け取る JSON で、transcript_path を含みます。組み込みガードが読み込まれる環境では、インストールした mod にあるこれらのイベントのフックは実行されません。

このフックは、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 が実行される順序

同じイベントのフックは 1 つのミドルウェアチェーンを形成します。各 mod の next は次の mod のフックを呼び出し、最後の next は Claude Code 独自の動作に到達します。最初の mod は最も外側です:他の mod より先にイベントを見て、他の mod より後に結果を見て、他の 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 つのモジュール内で、フックは register が on を呼び出した順序で実行されます。

設定フックが順序のどこで実行されるか

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

  • 管理設定からの PreToolUse フック:最初の mod の tool.call フックの前に実行されます。管理フックが呼び出しをブロックした場合、その呼び出しではどの mod の tool.call フックも実行されません。
  • 他のすべての設定ファイルおよびプラグインの hooks/hooks.json からの PreToolUse フック:最後の mod が next を呼び出した後、Claude Code 独自の動作の一部として実行されます。next を呼び出さずに tool.call に応答する mod はそれらの実行を妨げ、next を呼び出す mod は返す結果の中でそれらの決定を見ます。

tool.check は、PreToolUse フックが実行された後に発火します。tool.check フックの allow で置き換えられない権限ルールと PreToolUse フックの決定については、フックで権限を拡張するを参照してください。

失敗したフックを処理する

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

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

mod、イベント、および理由を示す 1 行が出力されます。例えば my-mod: tool.call hook skipped: threw Error: boom。それを読む場所はセッションに依存します。mod が何もしない理由を見つけ出すがリストアップしています。ツリーが検証に通らない ui.render フックは、間違った形の結果を返したフックとして扱われます。

呼び出しをブロックするフックをフェイルクローズにするには、代わりに応答する .catch エラーハンドラを追加します。ここで、guard はフック関数であり、ハンドラは next.called を確認して、guard が失敗した時点ですでに next を呼び出していたかどうかを判別します:

// on returns a registration, and .catch attaches a handler to that one hook
on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
  // guard had already called next, so return what came back
  if (next.called) return next(e)
  // next.error.kind says why the handler was asked, such as 'throw' or 'timeout'
  return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
})

guard が Bash 呼び出しでスローまたはタイムアウトすると、Claude Code は同じイベントでハンドラを呼び出します:

  • guard が next を呼び出す前に失敗した場合:コマンドは実行されず、Claude は末尾に種類が付いた deny のテキストを読み取ります
  • guard が next を呼び出した後に失敗した場合:ハンドラの next(e) は、コマンドを再実行することなく guard の呼び出しが生成した結果に解決され、Claude はその結果を読み取ります

ハンドラには、より短い独自の制限時間があります。ハンドラ自体がスローまたはタイムアウトした場合、Claude Code はハンドラがない場合と同様にフックをスキップします。guard が next を呼び出していなかった場合、コマンドは mod がない場合と同じように続行されます。

同じ形のハンドラは、prompt.submit や config.set のガードにも使えます。next.called が false の場合は、イベントリファレンスでそのイベントについて記載されている拒否を返します:prompt.submit には { drop: 'the reason' }、config.set には { deny: 'the reason' } です。

tool.check と plugin.register では、next が解決した後に返された拒否も有効なため、next.called を確認せずに返します:

tool.call では、next が解決した後に返された deny は結果を Claude に渡さずに保留します。

次のステップ