本文へ移動
Claude Tips

Mod のリファレンス

Mod(モッド)のファイルの形、フックの引数、全イベントと mods API のメソッド、描画先と要素、制限値、関連する設定・環境変数・コマンドの一覧と、使い方の要点です。

Mod(モッド、mod)が扱えるイベント、呼べる mods API のメソッド、描ける描画先を、Claude Code の CLI と Desktop アプリについて引く一覧です(v2.1.289 時点)。作り方は Mod を作る・試す、画面への描き方は Mod で画面に描く、入れ方と管理は Mod(モッド)を使うにあります。

補足

完全な資料は、Claude Code の TypeScript 型定義です。全イベント・メソッド・要素が、例付きで書かれています。GitHub 上のコピーは、手元の版より古いことがあります。食い違うときは、Claude Code が手元の版のために書き出すコピーを信じます(Mod を作る・試すの「使っている版の型定義を得る」)。

ファイル#

Mod は、次のファイルを持つプラグインのディレクトリです。

ファイル 必須 中身
.claude-plugin/plugin.json 必須 プラグインのマニフェスト。Mod が足す必須フィールドは無い
hooks/hooks.json 必須 modules:フックモジュールへの、このファイルからの相対パスを1つ持つ配列("modules": ["./register.js"])。hooks の下に設定フックも置ける
フックモジュール(hooks/register.js など) 必須 Mod の入口。register(on, options) を export する。拡張子は .js・.mjs・.cjs・.jsx・.ts・.mts・.cts・.tsx。ES モジュール
types/index.d.ts(マニフェストの types で名前を指す) $.state を使う、または mods API にネームスペースを足す Mod PluginState の値と、Mod が足すネームスペースを宣言する
名前が .test.ts か .test.tsx で終わるファイル 任意 claude plugin test が走らせるテスト

register は on と options を受け取ります。options は、マニフェストが宣言した userConfig フィールドの値で、既定値が入っています。

フック関数#

Mod は、register の中で on を呼んで、フック(イベントハンドラー)を登録します。on には、イベント名、省略できるマッチャー(イベントのフィールドに対する絞り込み)、フックを渡します(on('tool.call', { tool: 'Bash' }, async ($, e, next) => next(e)))。on は登録を返し、そのメソッド .catch(handler) でフックのエラーハンドラーを設定できます。

引数 中身
$ mods API。下の「mods API のメソッド」の全メソッド。ネームスペース、メソッドの順に完全な形で書く($.fs.read('notes.md'))
e イベントの入力。深く凍結された、そのままのデータ。変えるには、コピーを next に渡す
next(e) ミドルウェアのような次のハンドラー。このフックの後ろのフックを、続いて Claude Code の動きを実行し、イベントの結果で解決する
next.signal イベントが放棄されると中断される AbortSignal
next.origin イベントを発火した側の { plugin, tier }。Claude Code 自身は { plugin: 'engine', tier: 'core' }。Mod の tier は動く順序の優先グループで、prepend・user・append・builtin
next.budget フックの制限時間(ミリ秒)。next.budget.ms が全体、next.budget.remainingMs が残り
next.to(e, tier) 後ろの tier(append・builtin・core)まで飛ばす。next.to(e, 'append') はユーザーがインストールした Mod を飛ばす。呼べるのは prependPlugins か appendPlugins にある Mod だけ
next.error・next.called .catch ハンドラーの中だけ。next.error.kind は throw か timeout、next.error.message はエラーの文、next.called は失敗したフックが next を呼んでいたら true

イベントの扱い方#

同じイベントのフックはミドルウェアの連鎖になります。next(e) は次のハンドラー(別の Mod のフック、連鎖の最後では Claude Code 自身の動き)を呼び、その結果で解決します。next の使い方で、フックの働きは3つに分かれます。

働き 書き方
観察する 処理してから next(e) を返す。イベントのあとで動くなら、const result = await next(e) のあとで処理して result を返す
書き換える 変えたコピーを next に渡す(next({ ...e, text: e.text.trim() }) なら、後ろのハンドラーと Claude Code は整えたプロンプトだけを受け取る)。イベントは深く凍結されているので、フィールドへの代入は例外になる。結果を変えるなら、await next(e) の結果のフィールドを置き換えたコピーを返す
応答する next を呼ばずに結果を返す。連鎖が打ち切られ、後ろの Mod も Claude Code の動きも走らない。結果の形はイベントごとに決まっている(下のイベントの表)
javascript
// 観察:ツールが動く前に1行出し、イベントは変えずに渡す
on('tool.call', async ($, e, next) => {
  $.ui.log('Claude is about to use ' + e.tool)
  return next(e)
})

// 応答:next を呼ばないので、コマンドは動かない
on('tool.call', { tool: 'Bash' }, async () => {
  return { deny: 'Bash is turned off in this project. Use the file tools.' }
})

観察の例では、ツールが動く前にトランスクリプトへ ● my-mod: Claude is about to use Bash のような薄い行が出ます(my-mod はプラグイン名)。応答の例では、Claude は deny の文をツールの結果として読みます。

マッチャーで絞る#

on の第2引数のマッチャーは、イベントのフィールドと比べるオブジェクトです。すべてのフィールドが一致したときだけフックが動きます。

javascript
// 文字列は1つの値:Bash の呼び出しだけ
on('tool.call', { tool: 'Bash' }, hook)
// 配列はどれか1つ:Edit と Write の呼び出し
on('tool.call', { tool: ['Edit', 'Write'] }, hook)
// 正規表現はパターン:1つの MCP サーバーのすべてのツール
on('tool.call', { tool: /^mcp__github__/ }, hook)

イベント名にはワイルドカードも使えます。'classic.*' はすべての設定フックのイベント、'*' はテレメトリ以外のすべてのイベントです(テレメトリのイベントは、自分の名前と { to: 'collector' } のマッチャーで受けます)。

同じイベントは、マッチャーごとに1回だけ登録できます。マッチャー無しで session.start に on を2回呼ぶと、モジュールは on("session.start") is registered twice without a matcher で読み込みに失敗するので、セッション開始時の処理は1つのフックにまとめます。

ツール呼び出しを止める・直す#

tool.call は、Claude がツールを使う直前に発火します(サブエージェントと MCP ツールの呼び出しも含む)。ツール名は e.tool、引数は e のフィールド(Bash なら e.command)です。next(e) を呼ぶと、Claude Code が権限の確認をしてからツールを実行します。

javascript
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)
})

deny の文は Claude がツールの結果として読むので、Claude が従える指示として書きます。そのほかの扱い方です。

したいこと 書き方
実行後に動く await next(e) して結果を返す。拒否された呼び出しは { deny }、失敗した呼び出しは isError 付きで返る
引数を変える 変えた引数を next に渡す
やり直す もう一度 next(e) を呼ぶ(最初の結果の isError を見て、2回目を走らせられる)
自分で答える { result: 'Skipped by my-mod' } のように result を持つオブジェクトを、next を呼ばずに返す。権限の確認は出ず、ツールも動かないので、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 のような別の書き方には一致しません。

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

on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
  if (!RISKY.test(e.command)) return next(e)
  // 誰も答えなかったときに拒否になるよう、安全な答えから始める
  let answer = 'Refuse'
  try {
    // ユーザーが選ぶまで、ツール呼び出しはここで待つ
    answer = await $.ui.ask('Run this command? ' + e.command, ['Run it', 'Refuse'])
  } catch {
    // 質問が閉じられた、または claude -p で聞く相手がいない
  }
  if (answer !== 'Run it') {
    return { deny: 'The user declined this command. Ask before trying a different approach.' }
  }
  return next(e)
})
ユーザーの操作 結果
「Run it」を選ぶ フックが next(e) を呼び、そのあとに通常の権限の確認が走る
「Refuse」を選ぶ コマンドは動かず、Claude は deny の文を読む
答えを打つ $.ui.ask が打った文で解決する。Run it と比べているので、ほかの文は拒否になる
誰も答えない(質問を閉じる・「Chat about this」を選ぶ・claude -p の実行) $.ui.ask が拒否され、catch で答えが Refuse のままになる

注意

待つのは $.ui.ask のような mods API の呼び出しの中にします。その時間はフックの制限時間に数えられませんが、自分の Promise を待つ時間は数えられます。時間切れのフックは飛ばされるので、保留していたコマンドが実行されてしまいます。

ユーザーに聞く前に承認・拒否する#

tool.check は、権限ルールと設定フックが「実行してよいか」を決めたあとに発火します。next(e) はその決定(allow・ask・deny)で解決し、フックはそれか別の決定を返します。ツールの引数は e.input(Bash なら command)です。

決まったコマンドやパスなら、コードの要らない Bash(npm test) のような権限ルールで足ります。いまの Git ブランチや、別のフックが記録した値のように、その時の状況で決めたいときに tool.check を使います。

javascript
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 を拒みます。ほかのブランチやコマンドは、Mod が無いときと同じ決定です。コマンドの文字列で照合しているので、これは Claude への注意にとどまります。全員の main への push を止めるなら、Git のホスト側でブランチを保護します。

フックは allow も返せるので、管理設定の外の PreToolUse フックが止めた呼び出しも承認できます。どの決定が Mod より優先されるかは権限ルールにあります。

プロンプトを書き換える・足す#

prompt.submit のフックは、ターンが始まる前の各プロンプトを受け取ります。打たれた文は e.text です。

したいこと 返すもの
プロンプトを書き換える(トランスクリプトの発言も新しい文になる) next({ ...e, text: newText })
プロンプトのあとに、Claude だけが読む文を足す next({ ...e, context: [...(e.context ?? []), extraText] })
プロンプトを送らない { drop: 'the reason' }
javascript
on('prompt.submit', async ($, e, next) => {
  // pull request に触れないプロンプトはそのまま通す
  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)
  // 前のフックが足した文脈を残し、1行足す
  return next({ ...e, context: [...(e.context ?? []), 'Current branch: ' + git.stdout.trim()] })
})

open a PR for this change と打つと、発言はそのままで、Claude はそのあとに Current branch: feature/auth のような行も読みます。pull request に触れないプロンプトでは git も動きません。

Claude が読むそのほかのもの(システムプロンプトの節ごとの prompt.section、最初のメッセージと送る文脈の prompt.context、スキルの文の skill.prompt など)は、下のイベントの表にあります。こうしたフックが足す文がリクエストごとに変わると、プロンプトキャッシュが無効になります。

ターンを追う#

ターンは、1つのプロンプトに対して Claude がすることの全部です。

イベント 発火するとき フックでできること
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 を持つオブジェクトを返して、答えの下に1行出す

turn.step はストリームなので、フックは非同期ジェネレーターで書きます。yield* next(e) は、応答を流れてくるまま転送し、終わった結果として評価されます。

javascript
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
})

応答はいつもどおり画面に流れ、リクエストが終わるたびに、キャッシュから読んだトークン数と書いたトークン数の薄い行が出ます。result.usage には、Claude API が報告するトークン数(input_tokens・output_tokens・cache_read_input_tokens・cache_creation_input_tokens)と、答えた model が入っています。サブエージェントのリクエストでも動くので、メインの会話だけにするなら e.agentId を調べます。

設定フックのイベントを扱う#

設定ファイルに置くフック(コマンド・HTTP・プロンプト・エージェント)のイベントは、どれも classic. を前に付けた名前(classic.Stop・classic.SessionEnd・classic.PostToolUse など)で Mod からも扱えます。e は、設定フックが標準入力で受け取る JSON(transcript_path を含む)です。

javascript
on('classic.Stop', async ($, e, next) => {
  $.ui.log('Transcript saved at ' + e.transcript_path)
  // 渡すので、設定ファイルの Stop フックもいつもどおり動く
  return next(e)
})

Claude が応答を終えるたびに、トランスクリプトのファイルのパスが薄い行で出ます。next(e) を返すので、ターンの終わり方は変わりません。

イベントの一覧#

イベントは関わる対象ごとに分かれ、発火するときと、フックが返せるものを持ちます。turn.step と process.spawn のフックは非同期ジェネレーターで、そのほかのフックは非同期関数です。

「フックが返せるもの」の列は省略した書き方です。next(e) はイベントを変えずに渡します。next({ ...e, text }) は、名前を挙げたフィールドを変えたコピーを渡します(next({ ...e, text: e.text.trim() }))。オブジェクトは、next を呼ばずにイベントに答えます。reason のような語は、自分で書く文字列です({ deny: 'Use the file tools.' })。

ツール#

ツールのイベントは、Claude が行う各ツール呼び出しの前後で発火します。

イベント 発火するとき フックが返せるもの
tool.call ツールが動く直前 next(e)、{ deny: reason }、{ result }
tool.check Claude Code が呼び出しを実行してよいかを決めるとき(tool.call と PreToolUse のフックのあと)。next(e) は、ルール・権限モード・それらのフックが出した決定で解決する { decision }(allow・ask・deny)
tool.describe ツールの説明が最初に Claude へ送られるとき、ツールごとに1回 { description }。isDeferred を true にするとツール検索の背後に置き、false にすると最初から読み込む

プロンプトと Claude が読むもの#

プロンプトのイベントは、ユーザーが打つ文と、システムプロンプトやリマインダーのように Claude Code が自分で Claude へ送る文を扱います。

イベント 発火するとき フックが返せるもの
prompt.submit プロンプトが送信されるとき next({ ...e, text })、next({ ...e, context })、{ drop: reason }
prompt.fill・prompt.suggest 文が下書きとして、または薄い提案として、プロンプト欄に入る直前 文を変えた next(e)
prompt.edit ユーザーがプロンプト欄を編集したとき next(e)
prompt.compose Claude Code がシステムプロンプトを組み立てるとき { sections }(送る順に並べた { id, text, scope } の一覧)
prompt.section システムプロンプトの名前の付いた節ごとに1回。e.name は prompt.compose での節の id { text }。{ text: null } でその節を省く
prompt.context 会話ごとに1回、最初のメッセージと一緒に送る文脈について { blocks }
prompt.attachment Claude Code が、リマインダーなど自分のメッセージを Claude に足すとき。e.type が種類の名前で、型が宣言している種類では e.detail に文のもとになった事実が入る { text }。{ text: null } で省く
skill.prompt スキルの文が Claude のために展開されるとき { text }
attribution.text Claude Code がコミットか pull request の帰属の文を組み立てるとき { text }

コマンドと設定#

コマンドと設定のイベントは、コマンドが実行される・一覧に出るとき、/config の行が表示される・変わるときに発火します。

イベント 発火するとき フックが返せるもの
command.run コマンドが実行される直前 { text }、{}、next(e)
command.describe コマンドの一覧のために、コマンドごとに1回 { description, argumentHint, isHidden }
config.set /config の行が変わる直前 next({ ...e, value })、{ deny: reason }
config.describe /config の行ごとに1回 { label, description, isHidden }

ターン#

ターンのイベントは、1つの答えを、その中のモデルへの各リクエストも含めて、始めから終わりまで追います。

イベント 発火するとき フックが返せるもの
turn.start ターンの開始 next(e)
turn.step 1回のリクエストがモデルへ送られる直前 yield* next(e)、next({ ...e, model })、next({ ...e, effort })
turn.complete ターンの終了 next(e)、または答えの下に1行出す { text }

セッション#

セッションのイベントは、セッションの開始・終了・コンパクト・ほかのセッションとのメッセージのやり取りを表します。

イベント 発火するとき フックが返せるもの
session.start 読み込まれた Mod ごとに1回(最初のプロンプトの前)と、その Mod の読み直しのあと。/clear・/resume・/branch のあとは発火しない next(e)
session.end セッションの終了、または /clear・/resume・/branch の実行。e.reason は clear・resume・logout・prompt_input_exit・other(/branch は resume を報告する) next(e)
session.compact 会話がコンパクトされる直前 { skip: reason }
session.receive・session.send ほかのエージェントやセッションからメッセージが届く・届ける直前 receive は { consumed: reason }、send は { isDelivered: false, reason }
session.append 会話が保持する行(プロンプト・応答のブロック・ツールの結果・通知など)ごとに、保存の前に1回 行の content を書き換える next({ ...e, message })
session.attach・session.detach 別のアプリがセッションにつながる・切れる next(e)
session.measure 各ターンのあと、およびプランの上限の使用率が変わったとき next(e)

サブエージェント#

サブエージェントのイベントは、サブエージェントの種類が Claude に提示されるとき、サブエージェントやエージェントチームのチームメイトが起動する直前に発火します。

イベント 発火するとき フックが返せるもの
agent.offer サブエージェントの種類が Claude に提示されるとき { isOffered: false }(提示しない)
agent.spawn サブエージェントまたはエージェントチームのチームメイトが起動する直前。チームメイトでは e.isTeammate が true モデルを選ぶ next({ ...e, model })、または { deny: reason }

インターフェース#

インターフェースのイベントは、Claude Code が描画先を描くとき、Mod が描いたコントロールをユーザーが使うときに発火します。

イベント 発火するとき
ui.render 描画先が描かれる直前
ui.resolve Mod の読み込み時に、アプリ・描画先・Mod ごとに1回。結果は $.ui.resolve(e) が読む要素の表
ui.press・ui.input・ui.select Mod が描いた Button・Input・Select が使われたとき
ui.focus・ui.scroll フォーカスされたコントロール、またはペインか帯のスクロール位置が変わる直前
ui.close ペインが閉じる直前。e.id がペインで、e.origin.kind は plugin・person・unload
ui.message Client 要素が自分の Mod へデータを送るとき
ui.fault Mod が描いた Client 要素の読み込み・描画・実行が失敗したとき。e.phase は load・render・run、e.reason はエラーの文。Claude Code v2.1.289 以降が必要

別の Mod#

これらのイベントは、ほかの Mod が読み込まれるときに働きかけ、拒否したり、その Mod が受け取る mods API を変えたりします。

イベント 発火するとき フックが返せるもの
plugin.register フックモジュールが読み込まれる直前。e.uses は、claude plugin validate が出すとおりに、その Mod のイベント・mods API の呼び出し・環境変数・状態を並べる(呼び出しは $. を除いて書く:fs.read) { refuse: reason }
engine.create この Mod のために mods API が組み立てられるとき 変えた mods API(ネームスペースを足す、または外す)

テレメトリ#

テレメトリのイベントは、Claude Code が記録する利用記録について発火します。

イベント 発火するとき フックが返せるもの
telemetry.log・telemetry.mark テレメトリの記録が出力される直前、または機能の1回の使用が記録される直前。インストールした Mod では、on('telemetry.log', { to: 'collector' }, hook) のようにフィルター { to: 'collector' } を付ける。付けないと claude plugin validate が失敗する。* はこれらのイベントに一致しない next(e)、{ deny: reason }

設定フックのイベント#

設定フックのイベントはどれも classic.<Event>(classic.Stop・classic.PostToolUse など)という名前のイベントで、e はそのフックの標準入力の JSON です。

mods API の呼び出し#

mods API のメソッドはどれも、ネームスペースとメソッドの名前(fs.read・model.complete・ui.open など)のイベントでもあります。そのフックは、自分より後ろで動く Mod からの呼び出しを横取りし、next(e)・{ deny: reason }・{ value } を返せます。

フックの順序と失敗#

Mod が動く順序#

同じイベントのフックは、1本のミドルウェアの連鎖になります。各 Mod の next が次の Mod のフックを呼び、最後の next が Claude Code 自身の動きに届きます。最初の Mod がいちばん外側で、ほかの Mod より先にイベントを見て、後ろの結果も見て、ほかの Mod を走らせるかどうかを決めます。後ろの Mod は、前の Mod がイベントを見るのを止められません。

Claude Code は、各 Mod の出どころで連鎖を並べます。

  1. 組み込みのガード sec-default@builtin(/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 もその呼び出しを見ない
  • ほかのすべての設定ファイルと、プラグインの hooks/hooks.json の PreToolUse フック:最後の Mod が next を呼んだあと、Claude Code 自身の動きの一部として動く。next を呼ばずに tool.call に答える Mod は、これらを動かさない。next を呼ぶ Mod は、返ってきた結果にそれらの決定を見る

tool.check はこれらのフックと権限ルールが決めたあとに発火するので、そのフックは、後者のグループのフックが止めた呼び出しも承認できます。

フックが失敗したとき#

失敗したフックはセッションを壊さず、代わりに何が起きるかを決められます。.catch ハンドラーの無いフックが、例外を投げる・時間切れになる・形の違う結果を返すと、そのあとは next を呼んでいたかで決まります。

  • next を呼ぶ前に失敗した:Claude Code がそれを飛ばし、次のハンドラーが代わりに動く
  • next が解決したあとに失敗した:その結果が残り、2回目は何も動かない

Mod・イベント・理由を挙げる1行が出ます(my-mod: tool.call hook skipped: threw Error: boom)。どこで読むかは、Mod を作る・試すの「Mod が何もしない原因を探す」にあります。

呼び出しを止めるフックが失敗したら閉じる側(呼び出しを通さない)に倒すには、代わりに答える .catch エラーハンドラーを足します。guard は自分のフック関数です。

javascript
// on は登録を返し、.catch はそのフックだけにハンドラーを付ける
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 が動いている間、ハンドラーは動きません。Bash の呼び出しで guard が例外を投げるか時間切れになると、Claude Code は同じイベントでハンドラーを呼びます。ハンドラーが { deny } を返すのでコマンドは動かず、Claude は文の末尾に throw か timeout を付けて読みます。ハンドラーが無いと、Claude Code は guard を飛ばしてコマンドを実行してしまいます。ハンドラーには、より短い独自の制限時間があります。

mods API のメソッド#

mods API は、どのフックも受け取る $ 引数です。メソッドは $.ui のようなネームスペースに分かれています。表はネームスペースごとにメソッド名を並べたもので、$.ui の行の open は $.ui.open(...) の呼び出しを指します。

ネームスペース メソッド
$.plugin name、root(このプラグインの名前とディレクトリ)
$.ui resolve、invalidate、open、close、panes、focus、scroll、toast、status、log、notice、ask、copy、selection、blit
$.command register、run、list
$.tool register、call、check、list
$.agent register、spawn、list
$.model complete、fork、classify
$.prompt submit、read、fill、suggest、compose。submit({ text }) の文は、Mod を送り手と名指しする1文のあとに Claude が読む。submit({ text, asUser: true }) は、その1文なしで、ユーザー自身の言葉として送る
$.turn abort
$.session messages、cwd、root、model、turns、id、repo、surfaces、usage、version、compact、send、append、authorize。usage() は { startedAt, context, rateLimits, cost } を返す。context は tokens・window・percent を持ち、rateLimits は { kind, percentUsed, resetsAt } の一覧
$.config list、set
$.settings read
$.env get、set
$.fs read、write、list、exists、stat、ancestors。write はアトミックではなく、ファイルの中身をその場で置き換えるので、ほかのプロセスが書きかけのファイルを読みうる。複数のセッションが変えるデータは $.store に置く
$.store get、set、delete、keys。マシン上のすべてのセッションが共有するキーと値のストア
$.state リアクティブな状態:get、set。補助の atom・read・update・derive・memberOf は claude-code から import する
$.clock now、sleep、after、every
$.http fetch
$.process run、spawn
$.mcp call、connect。connect(server) は、自分のプラグインのマニフェストが挙げる MCP サーバーにつなぐ
$.audio play、speak
$.telemetry log、mark。記録が送られるのは、Claude Code か組み込みの Mod が呼んだときだけ

コマンドとツールを足す#

ユーザーが実行するコマンドも、Claude が呼ぶツールも、session.start のフックで登録します。Claude Code は最初のプロンプトの前にこのフックを待つので、最初のターンから使えます。

コマンド#

登録して、その名前の command.run を扱います。次は、日数を省略できる /standup の例です。

javascript
on('session.start', async ($, e, next) => {
  // description は / の一覧に出る説明
  await $.command.register({ name: 'standup', description: 'Summarize what changed today', argumentHint: '[days]' })
  return next(e)
})

on('command.run', { command: 'standup' }, async ($, e) => {
  // e.args はコマンド名のあとに打った文字(無ければ空文字)
  return { text: 'Summary for the last ' + (e.args || '1') + ' day(s): ...' }
})
  • / を打ったときの一覧に、説明付きで /standup が出る。argumentHint は、コマンドと空白を打ったあとに /standup [days] のように出る
  • 返した text はトランスクリプトに出て、Claude も読む。何も出さない(ペインを開くだけなど)なら {} を返す
  • Claude の作業中でも実行できるようにするには、登録に immediate: true を足す

注意

組み込みのコマンドが使っている名前(セッションで / を打つと見える)は使えません。$.command.register が "/focus" refused: it is the built-in /focus のような例外を投げ、例外を投げたフックは飛ばされるので、session.start のフックの残りも動きません。登録はフックの最後に置くか、try と catch で包みます。

ツール#

名前・Claude が読む説明・入力の JSON Schema を渡して登録します。Claude からは、mcp__・プラグイン名・アンダースコア2つ・登録した名前をつないだ長い名前で見えるので、呼び出しはその名前に絞った tool.call フックで扱います。次は my-mod というプラグインが、課題管理の票を引く ticket(完全な名前は mcp__my-mod__ticket)を足す例です。

javascript
on('session.start', async ($, e, next) => {
  await $.tool.register({
    name: 'ticket',
    // Claude はこの説明を見て、いつ呼ぶかを決める
    description: 'Look up a ticket by its id and return its title and status',
    // 必須の文字列 id を1つ受け取る
    inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
  })
  return next(e)
})

on('tool.call', { tool: 'mcp__my-mod__ticket' }, async ($, e) => {
  // ツールの引数は e のフィールドなので、id は e.id
  const response = await $.http.fetch('https://tickets.example.com/api/' + encodeURIComponent(e.id))
  // 失敗したことも Claude に伝わるよう、どちらでも結果を返す
  return { result: response.ok ? response.text : 'Lookup failed with status ' + response.status }
})

モデルを呼ぶ#

$.model.complete は、会話の外で、並べ替えや要約のような小さな仕事のために、セッションの認証情報で1つのプロンプトをモデルへ送り、返事で解決します。会話の履歴は含まれません。

javascript
on('command.run', { command: 'triage' }, async ($, e) => {
  const r = await $.model.complete({
    model: 'haiku',
    system: 'Reply with one word: bug, feature, or question.',
    prompt: e.args,
    // 1語で足りるので少なく。15秒で諦める
    maxTokens: 20,
    timeoutMs: 15000,
  })
  // r.text はモデルが答えたときだけあるので、先に r.isAnswered を見る
  const label = r.isAnswered ? r.text.trim() : 'unknown'
  return { text: 'Label: ' + label }
})

/triage the export button does nothing なら Label: bug のように出ます。モデルが答えなければ unknown です。

  • Claude API の失敗では呼び出しは拒否されない。r.isAnswered を調べ、false なら r.reason を読む
  • 組織が止めているモデルなど、Claude Code が送らないリクエストでは、呼び出しが拒否される
  • effort などほかのオプションは使っている版の型定義に、maxTokens の既定値は下の「制限」の表にある
  • $.model.fork({ prompt }) は、同じモデルとシステムプロンプトで、いまの会話に対して1つ質問する。大半は Claude API がプロンプトキャッシュから返す
  • どちらの呼び出しも、ユーザーのプランか API キーを使う

バックグラウンドで動かす#

フックは1つのイベントのために動き、実行時間に制限があります(next と mods API の呼び出しを待つ時間は、$.clock.sleep を除いて数えません)。1つのイベントより長く続く仕事(1分ごとに何かを調べる、など)は、session.start で始めるタイマーで動かします。

  • $.clock.every と $.clock.after は setInterval と setTimeout の代わりで、遅延のミリ秒を先に書く($.clock.after(5000, fn) は5秒後に fn を1回呼ぶ)。どちらも cancel() を持つタイマーを返す
  • await $.clock.now() はミリ秒の時刻を返す

次は、1分ごとに pull request のチェックを調べ、プロンプトの下に出す例です(summarize は、コマンドの JSON 出力を数語にまとめる自作の関数)。

javascript
on('session.start', async ($, e, next) => {
  // 60,000 ミリ秒ごとに呼ぶ(最初は1分後)
  $.clock.every(60_000, async () => {
    const status = await $.process.run(['gh', 'pr', 'checks', '--json', 'state'])
    // プロンプトの下の行を、最新のまとめで置き換える
    $.ui.status('checks: ' + summarize(status.stdout))
  })
  // タイマーを待たずに返すので、セッションはすぐ始まる
  return next(e)
})

タイマーのコールバックはイベントの外で動くので、ターンの合間も動き続け、ターンは始めません。例外を投げるとエラーはデバッグログへ行き、次の間隔でまた動きます。タイマーはモジュールの読み直しで止まります。フックの中の長く続く処理には、イベントが放棄されたとき(ユーザーが中断したときなど)に中断される AbortSignal の next.signal を渡します。

ターンを始めずにユーザーへ見せる呼び出しは次のとおりです。

呼び出し ユーザーに見えるもの
$.ui.status(text) 変えるまで残る、プロンプトの下の1行。⚠ と Mod の名前で始まる(⚠ my-mod: checks: 3 passing)
$.ui.toast(text) 右上のトースト通知。文の上に Mod の名前が出て、数秒で消える
$.ui.log(text) Claude が読まない、トランスクリプトの薄い行。● と Mod の名前で始まる(● my-mod: build finished)

Claude に動いてほしいものを見つけたら、$.prompt.submit({ text }) でプロンプトを送ってターンを始められます。Claude は、Mod を送り手と名指しする1文のあとにその文を読みます(asUser: true を足すと、その1文なしでユーザー自身の言葉として送ります)。呼び出しはセッションが空くのを待って新しいターンを始め、そのターンが始まったときに解決するので、Claude の作業中に動くハンドラーでは await しません。

セッション間でメッセージを送る・受ける#

Mod は、ほかの自分のセッションや、このセッションのサブエージェントへプレーンテキストのメッセージを送れ、出入りするメッセージを観察できます。送るのは $.session.send({ to, text }) で、SendMessage ツールと同じ配送です。to には次のどれかを渡します。

  • セッション:{ sessionId }
  • サブエージェント:$.agent.list() から得た { agentId }
  • 受け取ったメッセージの送り元の、文字列のアドレス

キューに入れば { isDelivered: true }、届かなければ { isDelivered: false, reason } で解決します(reason が理由)。

javascript
on('command.run', { command: 'ping' }, async ($, e) => {
  // e.args は /ping のあとに打ったセッション ID
  const sent = await $.session.send({ to: { sessionId: e.args }, text: 'Status? One line.' })
  // どちらでも解決するので、isDelivered で結果を見る
  if (!sent.isDelivered) $.ui.toast('Not delivered: ' + sent.reason)
  return {}
})

観察には session.receive と session.send を使います。どちらも next(e) を返せば、メッセージは変わらずに通ります。

イベント 発火するとき 使えるフィールド
session.receive このセッションにメッセージが届いたとき(Claude が読む前) e.text。e.origin.kind(別のセッションやエージェントなら peer か peer-send-message、ほかに task-notification・scheduled-trigger)。{ consumed: reason } を返すと Claude に届かない
session.send メッセージが出ていく直前(SendMessage ツールか Mod から) e.to、e.text、e.origin.kind(model か plugin)
  • 受信を断る設定のセッションは、session.receive が発火する前に断るので、フックには来ない
  • あなたの承認待ちで保留されたメッセージは、フックが先に受け取る。Mod は、まだ承認していないメッセージも読める
  • メッセージが配送されないと、フックの next(e) は拒否される
  • 送り手の名前は送り手が書いたとおりなので、判断の根拠にしない

ファイル・プロセス・ネットワークに届く#

フックモジュール自身には、Node.js の API も、setTimeout のようなタイマーのグローバルも、ネットワークやファイルへのアクセスもありません。ファイルシステム・プロセス・ネットワークには、mods API を通して、Claude Code を動かすユーザーと同じ権限で届きます。URL・TextEncoder・AbortController・crypto.subtle のような標準の JavaScript と Web の API は使えます。

ネームスペース できること
$.fs read(path)・write(path, text)・exists(path)・stat(path)・list(path) でファイルとディレクトリを扱う
$.process run(['git', 'status']) はコマンドを起動し、終了すると解決する。spawn は長く動くコマンドの出力をストリームで返す
$.http http か https での fetch(url, init)。本体を読み終えると { status, ok, headers, text } で解決する
$.store プラグイン自身の、セッションをまたいで残る JSON のキーと値のストア
$.env 環境変数の get と set。名前は文字列リテラルで書く
$.settings 設定ファイルと管理ポリシーが持つ内容の read
$.session messages() はトランスクリプトを { role, text, toolUses } の一覧で返す。作業ディレクトリ・モデルなども。usage() はコンテキストウィンドウの使用量とプランの上限を返す
$.mcp つながった MCP サーバーのツールを call する

ファイルとプロセスには、次の決まりがあります。

  • パス:相対パスは、セッションの作業ディレクトリを基準に解決される
  • $.fs.list:1つのディレクトリの項目を { name, kind, size, isLink } で返す。再帰はしない
  • $.process.run:引数の一覧を取り、シェルを使わない。終了コードによらず { exitCode, stdout, stderr } で解決する。プログラムが起動できない、またはタイムアウト(既定 30 秒)でも動いているときは拒否されるので、try と catch で包む

これらの呼び出しは、どれも $. を除いた名前($.fs.read なら fs.read)のイベントでもあります。連鎖の前にいる Mod がほかの Mod の呼び出しを観察・書き換え・拒否でき、組織が Mod の届く範囲を制限できるのはこのためです。

描画先#

描画先は、Claude Code のインターフェースにある拡張の差し込み口です。表の各行は、ui.render フックの e.component の値で、e.props のフィールドと、描くアプリを示します。e.surface は terminal か desktop です。

描画先 e.props e.requestId 描くアプリ
Pane title、isFocused、bodyColumns、placement、scroll、view ペインの id ターミナル、Desktop
AbovePrompt hasSurvey、isWorking、maxRows、bodyColumns、scroll、view 1つのインスタンス ターミナル、Desktop
UserMessage text、origin、isExpanded、および発言の出どころにより task か from メッセージの id ターミナル、Desktop
AssistantMessage 返事の文 メッセージの id ターミナル、Desktop
ToolUse、ToolResult、ToolGroup ツールの名前・入力・結果 ツール呼び出しの id ターミナル、Desktop
CommandOutput command、text メッセージの id ターミナル、Desktop
AskUserQuestion 質問と選択肢 ツール呼び出しの id ターミナル、Desktop
ToolProgress kind ツール呼び出しの id ターミナル
Spinner word、message、suffix、mode エージェントの id ターミナル、Desktop
TurnDuration word、durationMs メッセージの id ターミナル
InfoNotice text、command メッセージの id ターミナル
SessionMode modes 1つのインスタンス ターミナル、Desktop
PromptHint isDraft、isWorking、hint 1つのインスタンス ターミナル、Desktop

e.viewport は columns・rows・isFullscreen を持ちます。アプリがウィンドウを測るまでは存在しません。rows は、ペインの高さではなく、ウィンドウ全体の高さです。

木を描画先に合わせるには、フックで次の props を読みます。

  • Pane や帯の幅:e.props.bodyColumns に合わせて描く
  • トランスクリプトの横の Pane の高さ:e.props.placement が 'dock' のとき、e.props.scroll.bodyRows がペインの持つ行数
  • プロンプトの上の Pane の高さ:e.props.placement が 'inline' のとき、ペインは木に合わせて上限まで伸び、bodyRows は今見えている行だけを数える。$.ui.open の rows フィールドで別の上限を頼める

ペインより背の高い木は、全体でスクロールします。

要素#

要素は、ui.render フックが返す木の部品で、$.ui.resolve(e) から得ます。チェックマークは、そのアプリが描けることを表します。

要素 主な props ターミナル Desktop
Box key、フレックスのレイアウト、gap、padding、margin、width、height、borderStyle、backgroundColor、position、hover ✓ ✓
Text color、backgroundColor、bold、italic、underline、dimColor、inverse、wrap ✓ ✓
Button key、label、onPress、hotkey、plain、dimColor、autoFocus、action ✓ ✓
Link href、label ✓ ✓
Code コード(10,000 文字まで) ✓ ✓
Markdown text(10,000 文字まで)、key、dimColor、onLinkPress、pressableLinks ✓ ✓
Input key、label、placeholder、value、submitLabel、onSubmit、onInput、autoFocus ✓ ✓
Select key、label、options、value、onSelect、autoFocus ✓ ✓
Svg SVG ドキュメント(131,072 文字まで) ✓
Client module、key ✓ ✓
Raster key、columns(512 まで)、rows(256 まで)、cells ✓
Image PNG か RGBA のバイト列(2 MiB まで)、またはファイルパス ✓

Button の決まりが、さらにあります。

  • action は、Claude Code 自身のキーバインドのアクションの1つを指す。ユーザーのそのアクションへの割り当てが、複数キーの組み合わせ(コード)か修飾キー付きのキーのとき、そのキーでボタンが押される
  • 帯の中のボタンの数字の hotkey は、空のプロンプトにその数字だけを打って少し待ったときにも発火する
  • 1つの描画に同じ hotkey のボタンが2つあると、後のほうが得る
  • autoFocus は、どのコントロールでも true だけを受け付ける。切るには、prop を書かない

制限#

フックと mods API の呼び出しには、時間とサイズの制限があります。Claude Code は、時間の制限を超えたフックを飛ばし、サイズの制限を超えた呼び出しを拒否します。

制限 値
1つのイベントでのフック自身の実行時間(next の中と、$.clock.sleep 以外の mods API の呼び出しの中の時間は数えない) 10 秒。prompt.edit のフックは 50 ミリ秒
.catch ハンドラーの実行時間 1 秒
すべての session.end のフックの合計 SessionEnd フックの予算と同じ。変えなければ 1.5 秒で、設定の SessionEnd フックが終わった時点から数える
$.process.run のタイムアウト 既定 30 秒、最大 10 分
$.model.complete の maxTokens 既定 1024、最大 64,000 またはモデルの出力上限
$.fs.read と $.fs.write 1ファイル 4 MiB
Text の文字列の子 1つ 10,000 文字
$.store JSON で合計 4 MiB
$.session.messages() 新しい 4,096 件
$.ui.invalidate('ui.render') による再描画 1秒に 10 回まで。ターミナルで見えているペイン・展開した帯・プロンプト下のヒント行は 30 回まで。それより早い呼び出しはまとめられる
$.ui.toast { timeoutMs } を渡さなければ 4 秒表示
ユーザーが頼まずに開いたペイン ターミナルの幅が 144 列から配置される。ユーザーが一度開いたあとは 110 列
コマンド・ツール・サブエージェントの種類・ペインの名前 英字・数字・_・-、64 文字まで
claude plugin test の1テスト テストが timeoutMs を設定しなければ 5 秒

設定と環境変数#

Mod に関わる設定と環境変数です。「どこで」の列は、どの設定ファイルや環境から読まれるかを示します。

名前 どこで 働き
CLAUDE_CODE_PLUGIN_DIRS 環境変数、または ~/.claude/settings.json の env フラグを渡せないアプリのために、--plugin-dir と同じようにプラグインのディレクトリを読み込む。絶対パスを :(Windows では ;)で区切る
CLAUDE_CODE_PLUGIN_DIR_WATCH 環境変数 1 で、長く動く非対話のセッションが、保存のたびに --plugin-dir の Mod を読み直す
prependPlugins、appendPlugins 管理設定。管理設定が無いマシンで、Team か Enterprise プランでサインインしていないユーザーは、ユーザー設定でも可 acme-guard@acme-tools のようなプラグイン id の一覧。prependPlugins の Mod は、ユーザーがインストールするどの Mod より前に、appendPlugins の Mod は後に、並べた順で動く
allowManagedModsOnly 管理設定(組み込みのガードのオプション) 組織のものと数えられる Mod と、Claude Code に組み込まれた Mod だけが読み込まれる。ユーザーの設定フックは動き続ける
allowModsToOverrideDenyRules 管理設定(組み込みのガードのオプション) ユーザーがインストールした Mod が、deny ルールの拒否する呼び出しを承認できるようにする
allowManagedHooksOnly 管理設定 組織のものでないフックとインストールした Mod を止める
disableAllHooks どの設定ファイルでも 管理設定では、インストールしたプラグインの Mod もフックも動かない。自分の設定では、組織が管理するものは動き続ける
disableSideloadFlags 管理設定 起動時に --plugin-dir と --plugin-url を拒否する
pluginConfigs ユーザー設定か管理設定 Mod の userConfig の値を持つ。キーはプラグインの id(acme-guard@acme-tools)、--plugin-dir で読み込んだ Mod なら名前に @inline を付けたもの(first-mod@inline)

sec-default@builtin は Claude Code に組み込まれたガードで、/plugin とデバッグログでは cc-plugin-sec-default と出ます。管理設定のあるマシン、または Team か Enterprise プランでサインインしたユーザーでは、人がインストールするどの Mod より先に読み込まれます。管理設定の prependPlugins があるときは、その一覧が名前を挙げた場合だけ、挙げた位置で読み込まれます。

コマンド#

Mod を読み込む・調べる・テストするコマンドとフラグです。claude のコマンドはシェルで、/ のコマンドは Claude Code のプロンプトで実行します。<directory> は自分で打つパスで、角括弧は省略できる引数です。

コマンド 働き
/plugin 組み込みでない Mod が読み込まれていると、タブの下に 1 mod active · first-mod のような行を出す
claude plugin validate <directory> プラグインのマニフェストとフックモジュールを読み、エラー・扱うイベント・呼ぶ mods API を報告する。--strict は警告もエラーとして扱い、--json は機械で読める報告を出す
claude plugin test [directory] ディレクトリ(無ければ現在のディレクトリ)の下の、名前が .test.ts か .test.tsx で終わるファイルをすべて走らせる。テストが失敗すると終了ステータス 1
claude --plugin-dir <directory> 1セッションだけプラグインのディレクトリを読み込み、保存するとそのフックモジュールを読み直す。フラグを繰り返すと複数読み込める
/reload-plugins 実行したときにプラグインを読み直す

次に読むページ#

公式ドキュメント(英語)

2026年10月5日時点の内容をもとに、日本語でまとめています。

ページの一覧