はじめに
2026年10月1日、Anthropic が Claude Code の新しい拡張機構 mods を発表しました(公式ブログ)。mods は Claude Code の内部で動く JavaScript / TypeScript の関数で、ツール呼び出しの許可・拒否、プロンプトの書き換え、さらにはペインやボタンといった UI の追加まで行えます。
対象読者は、Claude Code の settings hook やプラグインをすでに使っていて「mods で何が変わるのか」「どこから試せばよいのか」を知りたい方です。公式ドキュメントの内容を整理したうえで、Claude Code v2.1.287 で最小構成の mod を作り、claude plugin validate / claude plugin test / claude -p を実際に動かした結果を載せます。
mods とは何か
公式ドキュメントの定義では、mod は「Claude Code の見た目と振る舞いを変えるプラグイン」で、中身はイベントハンドラ(以下フック)の集まりです(Mods overview)。ツール呼び出し・プロンプト送信・UI の描画といったイベントが起きると Claude Code がフックを呼び出し、フックはイベントを 観察する/書き換える/自分で応答する のいずれかを選びます。
既存の settings hook(settings.json に書くシェルコマンドや HTTP リクエスト)との最大の違いは、mods が Claude Code のプロセス内で関数として動く 点です。そのため settings hook ではできなかった次のことが可能になります。
- トランスクリプト横のペインやプロンプト上部のバンドに、タブ・ボタン・入力欄つきの UI を描く
- スピナーやツール呼び出しの行など、Claude Code 自身が描いている部分を差し替える
- ツール呼び出しを保留してユーザーに質問する、ツールを実行せずに結果を返す
- Claude のターンを介さずに即時実行される
/commandを追加する - 同じファイル内の変数を介してフック同士でデータを共有する
動作要件と動く場所
mods は Claude Code v2.1.287 以降 で既定で有効です。対応する実行環境は公式の表で次のように整理されています。
| 実行場所 | フックが動くか | mod の描画が表示されるか |
|---|---|---|
ターミナルの claude(JetBrains プラグイン含む) |
はい | はい |
| Desktop アプリの Code タブ(WSL 以外) | はい | はい(ターミナル専用要素を除く) |
| VS Code 拡張のチャットパネル | はい | いいえ |
claude -p と Agent SDK |
はい | いいえ |
| クラウドセッション | はい(プラグインが届く場合) | いいえ |
claude -p やクラウドセッションでもフックは動くため、UI を描かない「ガード系」の mod は CI や無人運用にもそのまま持ち込めます。
イベントの流れ
フックは on(イベント名, [マッチャー], async ($, e, next) => ...) の形で登録します。$ が mods API、e がイベントの入力(凍結済みのプレーンデータ)、next がミドルウェアと同じ「次のハンドラ」です。
同じイベントに複数の mod がフックした場合は、出どころ別の優先順(sec-default と組織の mod → ユーザーがインストールした mod → appendPlugins → その他の組み込み mod)で連なって実行され、同じモジュール内では on を呼んだ順になります。managed settings のあるマシンや Team / Enterprise プランでサインインしている場合は組み込み mod の sec-default が最初に読み込まれ、ユーザーがインストールした mod が組織のセキュリティポリシーを上書きできないようにしています(公式ブログ・Mods reference)。
主なイベントは次のとおりです(Mods reference)。
| カテゴリ | イベント例 | できること |
|---|---|---|
| ツール |
tool.call / tool.check / tool.describe
|
拒否・結果の差し替え・許可判定の上書き |
| プロンプト |
prompt.submit / prompt.section / skill.prompt
|
入力の書き換え・システムプロンプトの節単位の変更 |
| コマンド |
command.run / command.describe
|
独自 /command の実装 |
| ターン |
turn.start / turn.step / turn.complete
|
リクエストごとのモデルや effort の切り替え |
| セッション |
session.start / session.end / session.compact
|
初期化・後始末・圧縮のスキップ |
| UI |
ui.render / ui.press / ui.input
|
ペイン・バンドの描画と操作への応答 |
既存の settings hook も classic.PreToolUse のような classic.<Event> という名前のイベントとして mods から扱えます。
最小構成の mod を作って動かす
ここからは公式チュートリアル(Create a mod)の first-mod をベースに、Bash の特定コマンドを拒否するフックを1つ足した mod を作ります。検証環境は Claude Code on the web のクラウドコンテナ上の Claude Code 2.1.287(claude --version の出力は 2.1.287 (Claude Code))です。Node.js やビルド工程は不要で、Claude Code が .js / .ts をそのまま読み込みます。
ファイル構成
first-mod/
├── .claude-plugin/
│ └── plugin.json
├── hooks/
│ ├── hooks.json
│ └── register.js
└── tests/
└── first-mod.test.ts
plugin.json は通常のプラグインのマニフェストで、mods 固有の必須項目はありません。
{ "name": "first-mod", "version": "0.1.0", "description": "Counts Claude's tool calls and adds a /tally command", "author": { "name": "kai_kou" } }
hooks.json の modules キーがフックモジュールの場所を指し、このキーがあることでプラグインが mod になります。
{ "description": "The first-mod hooks module", "modules": ["./register.js"] }
フックモジュール
register.js は register(on) を export します。ツール呼び出しを数えるフック、rm -rf / を拒否するフック、/tally コマンド、スピナーへの件数表示の4種類を登録しています。
let calls = 0
export function register(on) {
on('session.start', async ($, e, next) => {
await $.command.register({ name: 'tally', description: 'Show how many tool calls Claude has made' })
return next(e)
})
on('tool.call', async ($, e, next) => {
calls += 1
$.ui.invalidate('ui.render')
return next(e)
})
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
if (/\brm\s+-rf\s+\/(\s|$)/.test(e.command ?? '')) return { deny: 'rm -rf / is blocked by first-mod' }
return next(e)
})
on('command.run', { command: 'tally' }, async () => {
return { text: 'Claude has made ' + calls + ' tool calls since this mod loaded' }
})
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
})
}
{ tool: 'Bash' } や { command: 'tally' } はマッチャーで、条件に合うイベントだけがそのフックに届きます。/tally のフックは next を呼ばずに { text } を返す「応答」型、スピナーのフックは e のコピーを next に渡す「書き換え」型です。
claude plugin validate で静的解析する
claude plugin validate は mod のコードを実行せずに、フックしているイベントと呼び出している mods API を列挙します。実行結果は次のとおりでした。
> ./register.js hooks: session.start, tool.call, tool.call{tool=Bash}, command.run{command=tally}, ui.render{component=Spinner}
> ./register.js calls: $.command.register, $.ui.invalidate
√ Validation passed
hooks: 行に5つのフックがマッチャーつきで並び、calls: 行に API 呼び出しが出ています。インストール前に他人の mod が何をするかを確認する用途にも、同じコマンドを使います。
イベント名を 'tool.calls' と1文字間違えた版も試すと、読み込む前にエラーで止まりました。
× Found 1 error:
> modules../register.js: bad-mod: .../bad-mod/hooks/register.js:7: "tool.calls" is not an event; ...
× Validation failed
静的解析で全フックを拾えるよう、公式ドキュメントは「$ を変数に代入しない」「イベント名は文字列リテラルで書く」「require ではなく ES モジュールで書く」といった書き方の制約を定めています。
claude plugin test で単体テストする
claude plugin test はセッションもサインインもネットワークも使わずに、イベントを擬似的に発生させてフックの挙動を検証します。テストコードは次のとおりです。
import { expect, test } from 'claude-code/testing'
test('/tally reports the tool calls the mod has seen', async ($, on) => {
on('tool.call', () => ({ result: 'ok' }))
await $.tool.call({ tool: 'Bash', command: 'ls' })
await $.tool.call({ tool: 'Read', file_path: 'README.md' })
const answer = await $.command.run({ command: 'tally', args: '' })
expect(answer.text).toBe('Claude has made 2 tool calls since this mod loaded')
})
test('rm -rf / is denied', async ($, on) => {
on('tool.call', () => ({ result: 'ok' }))
const r = await $.tool.call({ tool: 'Bash', command: 'rm -rf /' })
expect(r.deny).toBe('rm -rf / is blocked by first-mod')
})
テスト側の on('tool.call', () => ({ result: 'ok' })) が Claude Code 本体の代わりにツール呼び出しへ応答するため、実際のコマンドは一切実行されません。危険なコマンドを拒否するフックも安全に検証できます。
tests/first-mod.test.ts:
(pass) /tally reports the tool calls the mod has seen [87.46ms]
(pass) rm -rf / is denied [30.06ms]
2 pass
0 fail
Ran 2 tests across 1 file. [0.44s]
claude -p で実セッションに載せる
--plugin-dir を付けると、インストールせずにその場で mod を読み込めます。/tally はモデルのターンを経ずに実行されるコマンドなので、claude -p でも即座に結果が返りました。
claude -p "/tally" --plugin-dir ./first-mod
first-mod: Claude has made 0 tool calls since this mod loaded
出力の先頭にはプラグイン名が自動で付きます。
ツール拒否が実際の Claude のターンで効くかも確認しました。破壊的なコマンドを実セッションで流すのは避け、date コマンドだけを拒否する別の mod(date-guard)を用意して、Claude に date の実行を依頼しています。
export function register(on) {
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
if (/^\s*date\b/.test(e.command ?? '')) return { deny: 'date-guard: the date command is blocked in this session' }
return next(e)
})
}
claude -p "Run the bash command 'date' exactly once. Then quote verbatim the error text the tool returned." \
--plugin-dir ./date-guard --allowedTools Bash
The tool returned this error:
`<tool_use_error>date-guard: the date command is blocked in this session</tool_use_error>`
--allowedTools Bash で Bash を許可していても、mod の { deny } がツールの実行前に割り込み、拒否理由がそのまま Claude に届いています。なお claude -p は描画を行わないため、スピナーへの件数表示(ui.render)は今回の環境では確認していません。公式ドキュメントでは、対話セッションで Thinking · tool calls: 3… のように表示される様子が動画で示されています。
開発中のホットリロード
--plugin-dir で読み込んだディレクトリは Claude Code が監視しており、ファイルを保存するとフックモジュールが再読み込みされます。再読み込みのたびに register が再実行されるため、モジュール変数(上の例の calls)は 0 に戻ります。値を保持したい場合は $.state を使います。マーケットプレイスからインストールした mod はバージョン単位でキャッシュされるので、開発中はインストール版ではなく --plugin-dir で作業するよう公式が案内しています。
Claude に作らせる方法も用意されています。対話セッションで「プロンプトの上に現在の git ブランチを表示する mod を作って」のように依頼すると、組み込みの plugin-authoring スキルに沿って ~/.claude/dev-mods/{セッションID}/ 配下に mod が書かれ、ホットリロードを有効にするかを確認されます。
settings hook・Skill・MCP との使い分け
公式ドキュメントの比較表を要約すると、次のようになります。
| mod | settings hook | Skill | MCP サーバー | |
|---|---|---|---|---|
| 実体 | プラグイン内の関数(Claude Code のプロセス内で実行) | ライフサイクルイベントで動くシェルコマンド等 |
SKILL.md の指示 |
ツールを提供する外部プロセス |
| 変えられるもの | ツール呼び出し・プロンプト・コマンド・ターン・UI | ツール呼び出しやプロンプトの可否、引数と結果、追加コンテキスト | Claude の知識と行動 | Claude が使えるツール |
| UI を描けるか | はい | いいえ | いいえ | いいえ |
| 書く言語 | JavaScript / TypeScript | 任意のスクリプト+settings.json
|
Markdown | 任意 |
| 向いている場面 | ペインやバンド、独自コマンド、イベントの書き換え | 既存スクリプトでブロック・許可・ログをしたいとき | 同じ指示を何度も貼っているとき | 外部システムに接続したいとき |
1つのプラグインに4種類すべてを同梱できるので、「MCP サーバーで外部 API につなぎ、mod でその結果をペインに表示する」といった組み合わせも可能です。既に settings hook で足りている用途を無理に置き換える必要はなく、UI が欲しい・イベントを細かく書き換えたい・テストを書いて保守したい、といった場合に mods を選ぶのが自然です。
導入前に押さえておく注意点
mod は ユーザーの権限でそのまま動くコード です。公式ドキュメントは、インストールした mod が次のことをできると明記しています。
- ユーザー権限でのファイル読み書き・プロセス起動・ネットワーク通信
- 環境変数や設定ファイルに置いた API キーの読み取り
- すべてのプロンプトとツール呼び出しの閲覧・書き換え
- 確認なしでのツール呼び出しの承認(
askルールやPreToolUseフックのブロックを越えうる) - プランや API キーを使ったモデル呼び出し
一方、パーミッションプロンプトの表示内容は mod から変更できない仕様です。信頼できる作者・マーケットプレイスの mod だけを入れ、入れる前に claude plugin validate で hooks: と calls: を確認するのが基本の運用になります。
無効化の手段も段階的に用意されています。
| 範囲 | 方法 |
|---|---|
| 特定の mod だけ |
/plugin の Installed タブで無効化・アンインストール |
| そのセッションの全 mod |
claude --safe-mode で起動 |
| 全セッションの自分の mod |
~/.claude/settings.json に "disableAllHooks": true(settings hook とカスタムステータスラインも止まる) |
組織では managed settings の allowManagedModsOnly・prependPlugins・disableSideloadFlags などで読み込む mod を制限できます。早期アクセス期間に使われていた CLAUDE_CODE_ENABLE_FUNCTION_HOOKS は v2.1.287 以降では無視されるため、設定している場合は削除するよう公式が案内しています。
まとめ
- mods は Claude Code のプロセス内で動く JS / TS のフックで、ツール呼び出し・プロンプト・コマンド・UI を観察・書き換え・応答できる
- 必要なのは
plugin.json・hooks.json・フックモジュールの3ファイルで、v2.1.287 以降の CLI と Desktop で既定で有効 -
claude plugin validateで静的解析、claude plugin testでコマンドを実行しない単体テスト、--plugin-dirでインストールなしの読み込みとホットリロードができる - フックは
claude -pやクラウドセッションでも動くため、UI を持たないガード系 mod は無人運用にも使える - ユーザー権限でそのまま動くコードなので、導入前に
validateで中身を確認し、信頼できる配布元に限定する
組み込み mod の /diff や sec-default のソースは anthropics/claude-code の mods ディレクトリ で公開されています。自作する前に読んでおくと、ペインの描き方やポリシー mod の書き方の参考になります。