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 の動きも走らない。結果の形はイベントごとに決まっている(下のイベントの表) |
// 観察:ツールが動く前に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引数のマッチャーは、イベントのフィールドと比べるオブジェクトです。すべてのフィールドが一致したときだけフックが動きます。
// 文字列は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 が権限の確認をしてからツールを実行します。
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 のような別の書き方には一致しません。
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 を使います。
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' } |
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) は、応答を流れてくるまま転送し、終わった結果として評価されます。
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 を含む)です。
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 の出どころで連鎖を並べます。
- 組み込みのガード
sec-default@builtin(/pluginではcc-plugin-sec-default)、組織がprependPluginsに並べた Mod、そのあとに、組織のものと数えられappendPluginsにない Mod - 自分がインストールした Mod
- 組織が
appendPluginsに並べた Mod - 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 は自分のフック関数です。
// 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 の例です。
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)を足す例です。
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つのプロンプトをモデルへ送り、返事で解決します。会話の履歴は含まれません。
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 出力を数語にまとめる自作の関数)。
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 が理由)。
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 |
実行したときにプラグインを読み直す |
次に読むページ#
- Mod を作る・試す:作り方・型定義・validate・自動テスト・トラブルシューティング
- Mod で画面に描く:ペイン・帯・ボタン・入力欄・要素の見本
- Mod(モッド)を使う:入れ方・信頼・オンオフ・組織での管理
公式ドキュメント(英語)
2026年10月5日時点の内容をもとに、日本語でまとめています。