Claude Code modsは、AIへの指示文を増やす仕組みだけではありません。Claude Code自身のイベントを受け取り、ツール呼び出しや画面表示へコードで介入する拡張です。
本記事では「ツールを何回呼ぼうとしたか」を表示する小さなmodを例に、next(event)で元の動作を通す設計と、単体テストで確かめられる範囲を整理します。2026年10月6日の公式資料を確認し、サンプルはNode.jsのモックで実行しました。Claude Code内での読み込み・描画・課金は未検証です。
まず、どの拡張を使うべきか
| 必要なこと | 候補 | 主な理由 |
|---|---|---|
| 毎回貼る手順を共通化する | Skill | 読み込ませる知識と手順を整理できる |
| 外部システムを操作する道具を追加する | MCP server | 外部ツールとの境界を作れる |
| 既存スクリプトでイベントを記録する | settings hook | CLIの外側にある処理を呼べる |
| イベントに介入し、Claude CodeのUIも変える | mod | Claude Code内で動く関数として組み込める |
公式のMods overviewは、modsをplugin内のJavaScript/TypeScriptハンドラーとして説明しています。pluginは配布・読み込みの単位であり、Skill・MCP・modの役割を同一視しない方が構成を整理しやすくなります。
たとえば「テストを実行してから完了報告する」という指示だけならSkillで足ります。テスト実行の試行回数を画面に出したい場合はmodが候補です。ただし、試行回数の表示が、そのテスト結果の正しさを保証するわけではありません。
今回は試行数だけを観測する
tool.callのイベントを受け取る時点では、下流のツールが成功したかはまだ分かりません。そこで変数名も表示もattemptsにします。
ここで数えるのは、このハンドラーへ到達したイベントです。実際のネットワーク要求数、全modを含む実行回数、モデルのトークン量、成果物の完成数ではありません。他のmodが先にイベントを処理する場合もあり、表示名に測定対象を残す必要があります。
最小ファイル構成
公式のCreate a modにある構成へ合わせます。
tool-attempt-counter/
├── .claude-plugin/plugin.json
└── hooks/
├── hooks.json
└── register.js
plugin.json:
{"name":"tool-attempt-counter","version":"0.1.0","description":"Observe tool call attempts"}
hooks.json:
{"description":"Attempt counter hooks module","modules":["./register.js"]}
register.js:
export function register(on) {
let attempts = 0;
on('tool.call', async ($, event, next) => {
attempts += 1;
$.ui.invalidate('ui.render');
return next(event);
});
on('ui.render', { component: 'Spinner' }, async ($, event, next) => {
const suffix = event.props?.suffix ?? '';
return next({
...event,
props: { ...event.props, suffix: `${suffix} · attempts: ${attempts}` },
});
});
}
イベントをそのままnextへ渡しているので、このmodはツール名や引数を書き換えません。UI側はコピーしたpropsにsuffixを追加し、既存のlabelなどを維持します。カウンターはmodが登録されている間のメモリ上の値であり、再起動後の累積値ではありません。
Node.jsで確かめたこと
テスト用のonはイベントハンドラーをMapへ登録し、nextの代わりに関数を渡します。確認したケースは、正常に進む呼び出し2件と、下流が失敗する呼び出し1件です。テストはtest.mjsをpluginのルートへ保存し、Node.jsで実行できます。
import assert from 'node:assert/strict';
import { register } from './hooks/register.js';
const handlers = new Map();
register((name, matcherOrHandler, handler) => {
handlers.set(name, handler ?? matcherOrHandler);
});
let invalidations = 0;
const api = { ui: { invalidate: () => { invalidations += 1; } } };
const event = { tool: 'Read', marker: { preserved: true } };
const results = [];
for (let i = 0; i < 2; i++) {
const result = await handlers.get('tool.call')(api, event, received => {
assert.equal(received, event);
return 'continued';
});
results.push(result);
}
const failure = new Error('downstream failed');
await assert.rejects(
handlers.get('tool.call')(api, event, () => Promise.reject(failure)),
error => error === failure,
);
const spinner = { props: { suffix: ' original', label: 'Thinking' }, id: 4 };
const drawn = await handlers.get('ui.render')(api, spinner, received => received);
assert.equal(drawn.props.suffix, ' original · attempts: 3');
assert.equal(drawn.props.label, 'Thinking');
assert.equal(drawn.id, 4);
assert.equal(spinner.props.suffix, ' original');
assert.equal(invalidations, 3);
assert.deepEqual(results, ['continued', 'continued']);
console.log(JSON.stringify({attempts: 3, successes: 2, failures: 1, eventIdentityPreserved: true, errorPropagated: true, originalUiPropsPreserved: true, scope: 'Node mock, not Claude Code integration'}, null, 2));
node test.mjs
2026年10月6日、macOS/Node.js v26.9.0で実行し、以下を確認しました。
- 下流へ渡したイベントは元のオブジェクトと同一。
- 成功2件・失敗1件でも試行数は3。
- 下流の例外を握りつぶさず、同じ例外を返す。
- 元のSpinnerのpropsを変更せず、labelと既存suffixを保つ。
このテストではClaude CodeのAPI実装、ハンドラーの並び順、実際のSpinner表示を再現していません。Nodeで通ったことと、Claude Codeの実環境で動くことは別の検査です。公式にはmodのテスト手順もあります。現行版で検証を追加する際は、その手順と生成された型定義を確認します。
実環境へ読み込む前の確認
公式資料の対象は、ターミナルではClaude Code v2.1.287以降です。DesktopのCodeタブではv2.1.286以降が示されています。描画が出る場所とハンドラーが実行される場所には差があり、VS Codeのチャットパネルや非対話処理で同じ表示を期待しないようにします。
手元で試す場合のコマンドは次の形です。本記事の制作では実行していません。
claude --version
claude plugin validate ./tool-attempt-counter
claude --plugin-dir ./tool-attempt-counter
validateでは構成だけでなく、どのイベントを扱い、どのAPIを呼ぶかを確認します。表示用modであっても、mod全体が隔離されているわけではありません。公式には、ユーザー権限でファイルやネットワークへアクセスでき、モデル呼び出しによって利用量を消費し得ることが説明されています。
まずは今回のように、ファイル書き込み、モデル呼び出し、外部通信を含まない観測処理へ限定すると、レビューする対象を小さくできます。これは権限を制限する仕組みではなく、コードの責務を減らす設計上の選択です。
導入を判断するポイント
欲しいのが追加の知識ならSkill、外部操作ならMCP、既存スクリプトの連携ならsettings hookから検討します。Claude Codeの内側にあるUIやイベントを扱う必要があるときにmodを選ぶと、拡張の役割が明確になります。
modを作る場合も「試行」「成功」「完成」を分けて記録してください。見栄えのよいメーターほど、何を測っていないかまで表示・説明しておくと、開発の判断に使いやすくなります。
参考資料・関連解説
- Customize Claude Code with mods(発表:2026年10月1日)
- Mods overview
- Create a mod
- Test a mod
仕様確認:2026年10月6日。コード実行はローカルのモックテストのみです。