1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code mods 入門、TypeScript で挙動と UI を拡張する

1
Posted at

はじめに

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 固有の必須項目はありません。

first-mod/.claude-plugin/plugin.json
{ "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 になります。

first-mod/hooks/hooks.json
{ "description": "The first-mod hooks module", "modules": ["./register.js"] }

フックモジュール

register.js は register(on) を export します。ツール呼び出しを数えるフック、rm -rf / を拒否するフック、/tally コマンド、スピナーへの件数表示の4種類を登録しています。

first-mod/hooks/register.js
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 はセッションもサインインもネットワークも使わずに、イベントを擬似的に発生させてフックの挙動を検証します。テストコードは次のとおりです。

first-mod/tests/first-mod.test.ts
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 の実行を依頼しています。

date-guard/hooks/register.js
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 の書き方の参考になります。

参考リンク

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?