グラレコ
上のグラレコは、この記事の内容を1枚にまとめたものです。mods の正体、イベントに対する3つの動き、公式サンプルの Token Weather・Blast Radius・Replay Theater、そして安全に使うための注意点までを並べています。本文で1つずつ見ていくので、まずは全体の地図としてご覧ください 🗺️
はじめに
Claude Code に mods(モッド)という拡張の仕組みが加わりました。mod は、Claude Code のセッションの中で動く小さな JavaScript または TypeScript のファイルです。セッションで起きていることを見守ったり、Claude Code の動きを変えたり、ターミナルやデスクトップアプリに独自の表示を描いたりできます。
Anthropic は 2026年10月1日に、claude.dev ブログで「Getting started with Claude Code mods」を公開しました。著者は Addy Osmani さんです。あわせて、公式ドキュメントに「Mods overview」などのページ群が、claude.dev には mods の紹介ページ(claude.dev/mods)が用意されています。
この記事では、それらの公式情報をもとに、mods の仕組み、最初の1本の作り方、公式サンプルから学べる書き方、そして安全に使うための注意点を整理します 🧭
この記事でわかること 📌
- mods とは何か、設定ファイルのフック・スキル・MCP サーバーとどう違うのか
- フックがイベントに対してできる3つの動き(観察・書き換え・応答)
- 公式サンプル Token Weather を、空のフォルダから作る6つの手順
- Blast Radius と Replay Theater から学べる、一歩進んだ書き方
- mod が持つ権限と、インストール前に中身を確かめる方法
- mods を止める方法と、組織での管理の入口
想定読者は、Claude Code を日常的に使っていて「この表示がいつも欲しい」「このコマンドの前には一度止まってほしい」と感じたことのある開発者の方です。JavaScript の基本が読めれば、コード例も追えるように書いています。
mods をひとことで言うと 🧩
公式ブログは、mods を次のように説明しています。
Under the hood, mods are hooks that ship inside plugins, and each one sees every event in your session as it happens.
(内部的には、mods はプラグインの中に入って配布されるフックで、それぞれがセッションで起きるすべてのイベントを、起きたその時点で見ています。)
Claude Code には、これまでも設定、パーミッションのルール、スラッシュコマンド、スキル、ステータスラインといったカスタマイズの手段がありました。mods はそこからさらに一歩進んで、Claude Code の動きそのものを書き換えたり置き換えたりでき、独自の表示まで描ける 仕組みです。
以下の図は、mod が Claude Code のどこに入り込むのかを示しています。
ポイントは、mod が Claude Code の外ではなく中で動く ことです。ドキュメントの言葉を借りると、設定ファイルのフック、スキル、ステータスライン、MCP サーバーは Claude Code の外から働きかけるもので、スクリプトを実行したり、Claude に文章やツールを渡したりします。mod は中で動くので、それらにはできないことができます。
mod にできること
ドキュメントの「What a mod can do」には、次の5つが挙がっています。
| できること | 具体例 |
|---|---|
| 使える表示を描く 🖼️ | 会話の表示の横に開く表示枠(pane)や、プロンプトの上の帯(band)。タブ、ボタン、テキスト入力も置ける |
| Claude Code 自身の表示を描き直す 🎨 | ツール呼び出しの行、スピナー、Claude が質問するときのダイアログを置き換えたり、見た目を変えたりする |
| ツール呼び出しやリクエストに割り込む ✋ | ユーザーに確認する間ツール呼び出しを止めておく、ツールを動かさずに自分で答える、ある1回のリクエストだけ別のモデルに送る |
| コマンドで自分のコードを動かす ⌨️ | Claude のターンを使わずに、すぐ自分の関数が動く /コマンド。Claude が作業中でも動く |
| フック同士でデータを共有する 🔗 | あるフックがツール呼び出しを数え、別のフックがその数をスピナーの横に出す |
設定ファイルのフック・スキル・MCP との違い
Claude Code には似た名前の仕組みがいくつかあるので、ドキュメントの比較表を整理しておきます。
| mod | 設定ファイルのフック | スキル | MCP サーバー | |
|---|---|---|---|---|
| 正体 | プラグインに入った関数。Claude Code 自身のプロセスで呼ばれる | ライフサイクルのイベントで実行されるシェルコマンド、HTTP リクエスト、またはプロンプト | Claude が読む指示をまとめた SKILL.md
|
Claude にツールを渡す外部のプロセスやサービス |
| 変えられるもの | ツール呼び出し、プロンプト、コマンド、ターン、画面の描画 | ツール呼び出しやプロンプトを通すかどうか、ツールの引数と結果、Claude に追加する文脈 | Claude が知っていること・すること | Claude が使えるツール |
| 画面に描けるか | できる | できない | できない | できない |
| 書くもの | JavaScript / TypeScript | スクリプトと settings.json の項目 |
Markdown | 任意の言語のサーバー |
| 向いている場面 | 表示枠や帯、独自コマンドが欲しい。イベントを書き換えたい | 手元のスクリプトでイベントを止める・通す・記録する | 同じ指示を何度もチャットに貼っている | Claude を外部のシステムにつなぎたい |
ブログでは、設定ファイルのフックとの違いがもう少し具体的に書かれています。設定ファイルのフックはイベントごとにシェルコマンドを起動し、標準入出力で JSON をやり取りします。mod は一度読み込まれるとセッションの中に居続けるので、状態を持ち続けたり、イベントに合わせて表示を更新したり、Claude Code に「表示枠を開いて」「このプロセスを動かして」と頼み返したりできます。
どれを使うか迷ったときは、次のように考えるとわかりやすいです。
なお、1つのプラグインにこれらを全部入れることもできます。mod をスキルや MCP サーバーと同じプラグインで配布できる、ということです。
Claude Code 自身も mods で作られている
面白いのは、Claude Code 自身の機能の一部が mods として作られていることです。ドキュメントには、/plugin の Installed タブで Built-in として表示される組み込みの mod が並んでいます。
/plugin での名前 |
役割 |
|---|---|
cc-plugin-agents-md |
AGENTS.md をプロジェクトの指示として読み込む |
cc-plugin-diff |
/diff を引き受けて、未コミットの変更を表示枠に描く |
cc-plugin-plugin-authoring |
mod を書くための plugin-authoring スキルを Claude に渡す(スキルだけで、mod のコードは入っていない) |
cc-plugin-sec-default |
組織が管理している設定を、ユーザーが入れた mod から守る |
cc-plugin-telemetry |
Claude Code と組み込み mod の分析用の記録を送る |
cc-plugin-you-should-know |
長めの作業中に横で見守るエージェントを動かし、見落としそうなことをプロンプトの上に知らせる(既定では無効) |
このうち diff・agents-md・sec-default・telemetry のソースは、テスト付きで anthropics/claude-code リポジトリの mods/ ディレクトリ に公開されています。Claude Code のチームが mod をどう書いているかを、そのまま読めるわけです(自分で書き始める前に、ここを覗いておくとかなり参考になります)。
仕組み:フックの鎖と3つの動き ⚙️
mod のファイル構成
mod は、JavaScript または TypeScript のモジュールに動きが入っている、Claude Code のプラグインです。ドキュメントによると、小さな mod は3つのファイルでできています。
first-mod/
├── .claude-plugin/
│ └── plugin.json # プラグインのマニフェスト
└── hooks/
├── hooks.json # コードのファイルを指す
└── register.js # フックを登録するコード
それぞれの役割は次のとおりです。
-
plugin.json:普通のプラグインと同じマニフェストです。 -
hooks.json:modulesという項目で、コードのファイルを1つだけ指します。 -
register.js:register(on, options)という関数を公開し、その中でon(イベント名, 条件(省略可), フック)を呼んでフックを登録します。
フックはみんな同じ形
ブログによると、どのフックも次の同じ形をしています。
on("tool.call", { tool: "Bash" }, async ($, e, next) => {
// $ mods の API(ui, session, state, store, fs, process, clock, http, tool, command, model など)
// e このイベントの入力(ただのデータ)
// next e を他のプラグインへ、最後は Claude Code 自身の動きへ渡す
return next(e);
});
3つの引数を覚えておけば、あとはほとんど同じ考え方で読めます。
-
$:mods の API です。画面への描画、セッションの情報、状態の保存、ファイル、プロセス、時計、HTTP、ツール、コマンド、モデルなど、外の世界とのやり取りは全部ここを通ります。 -
e:イベントの中身です。ツール呼び出しなら、どのツールにどんな引数が渡されたか、といった情報が入っています。 -
next:イベントを次へ渡す関数です。
モジュールのコードは専用の実行環境で動き、DOM も Node の API もありません(ファイルやネットワークに直接は触れません)。そのため、自分のコードの外のことはすべて $ を通します。なお、これは OS レベルの隔離ではありません。この点は後半の安全の節で説明します。ドキュメントによると、外とのやり取りがこの1か所に集まっているからこそ、Claude Code はインストール前に「この mod が何をするか」を一覧にできます(この話は後半の安全の節でもう一度出てきます)。
フックは「順番に受け渡す鎖」になっている
フックは、Web アプリのミドルウェアと同じように鎖の形でつながります。以下の図は、1つのイベントが鎖を通る様子を示しています。
自分のフックが先に動き、next(e) でイベントを次のプラグインに渡し、いちばん奥で Claude Code が本来やるはずだったことを行います。結果は逆の順番で戻ってきます。
3つの動き:観察・書き換え・応答
この鎖の中で、フックができることは3種類です。ブログの表を日本語にまとめると、次のようになります。
| 動き | 書き方 | 例 |
|---|---|---|
| 観察(Observe)👀 | const r = await next(e); /* 見る */ return r |
ファイルの編集をすべて記録する。ターンが終わるたびに数値を測る |
| 書き換え(Rewrite)✏️ | return next({ ...e, command: safer }) |
鎖の後ろが見る内容を変える |
| 応答(Answer)🛑 |
next を呼ばずに return { deny: "…" }
|
ツール呼び出しを断る。コマンドやツールに自分で答える |
図にすると、違いがはっきりします。
扱えるイベントは、ツール呼び出し、送信されたプロンプト、ターンの開始と終了、セッションの開始と終了、スラッシュコマンド、そして ui.render(画面の各部品が描かれるたびに起きるイベント)など幅広く用意されています。
いちばん小さな mod の例
ドキュメントの「How a mod works」には、ツール呼び出しを数えてスピナーの横に表示する、完全な register.js が載っています。日本語のコメントを付けて引用します。
// 下の2つのフックで共有する数
let calls = 0
// mod が読み込まれたときに Claude Code が1回だけ呼ぶ
export function register(on) {
// Claude がツールを使う直前に毎回動く
on('tool.call', async ($, e, next) => {
calls += 1
// 新しい数が出るように、画面を描き直してもらう
$.ui.invalidate('ui.render')
// ツールは普段どおり動かす
return next(e)
})
// Claude Code がスピナーを描くたびに動く
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
// Claude Code のスピナーはそのままに、言葉の後ろに数を足す
return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
})
}
ポイントは3つです。
-
tool.callのフックは 観察 です。数を1つ増やしてから、ツールはそのまま動かしています。 -
ui.renderのフックは 書き換え です。スピナーのpropsにsuffixを足して、Claude が作業している間にThinking · tool calls: 3…のように表示させています。 - 2つのフックは、ファイルの先頭の
callsという変数を共有しています。
どこで動くか 🖥️
ドキュメントによると、フック自体はプラグインを読み込んだすべてのセッションで動きますが、描画が表示される場所は限られています。
| Claude Code を動かす場所 | フックは動くか | 描いたものは表示されるか |
|---|---|---|
ターミナルの claude(エディタ内のターミナル、JetBrains プラグインを含む) |
動く | 表示される |
| デスクトップアプリの Code タブ(WSL セッションを除く) | 動く | 表示される(ターミナル専用の部品を除く) |
| デスクトップアプリの WSL セッション | 動かない(プラグインが使えない) | 表示されない |
| VS Code 拡張のチャットパネル | 動く | 表示されない |
claude -p と Agent SDK |
動く | 表示されない |
| claude.ai やモバイルアプリからの Remote Control | 手元のマシンのセッションで動く | 手元のマシンのターミナルに表示される |
| クラウドのセッション | プラグインがクラウドに届いていれば動く | 表示されない |
描画する mod は、自分がどのアプリで動いているかを確認できます。描けない場所では、会話の中に1行出したり、コマンドの返答のテキストにしたりと、代わりの形に切り替えられます。
使い始めるための条件
ブログとドキュメントによると、mods は 既定で有効 になっていて、何かを有効にする操作は不要です。必要なバージョンは次のとおりです。
| 使う場所 | 必要なバージョン | 確認方法 |
|---|---|---|
| ターミナル | Claude Code v2.1.287 以降 | シェルで claude --version
|
| デスクトップアプリ | 同梱の Claude Code v2.1.286 以降 | Code タブのローカルセッションで /status を開き、Claude Code の行を見る |
⚠️ 早期アクセスの期間に
CLAUDE_CODE_ENABLE_FUNCTION_HOOKSという環境変数を設定していた場合は、削除するようドキュメントに書かれています。v2.1.287 以降はこの変数を無視するので、0にしても mods は止まりません。
最短ルート:Claude に作ってもらう 🤖
ここからは、公式ブログが題材にしている Token Weather を作っていきます。Token Weather は、ターンが終わるたびにコンテキストウィンドウの埋まり具合を読み取り、プロンプトの上に1行の「天気予報」を描く mod です。
| 使用率 | 予報 |
|---|---|
| 25% 未満 | ☀ Clear(晴れ) |
| 25〜49% | ☁ Cloudy(くもり) |
| 50〜74% | ☂ Showers(にわか雨) |
| 75〜89% | ☇ Storm(嵐) |
| 90% 以上 | ↯ Compact soon(そろそろ圧縮) |
ブログによると、コードはおよそ80行です。ただ、手で書く前に知っておきたいのが「Claude に頼めば作ってもらえる」という最短ルートです。Claude Code は mod の書き方を知っているので、欲しい mod を説明するだけで済みます。ブログに載っているプロンプトを日本語に訳すと、次のような内容です。
token-weather という Claude Code の mod を作ってください。
コンテキストウィンドウの埋まり具合を、プロンプトの上の帯にリアルタイムの天気予報として表示します。
1行に表示する内容:
- コンテキストの埋まり具合に応じた天気のアイコンと言葉:
25% 未満は ☀ Clear(黄)、25〜49% は ☁ Cloudy(シアン)、50〜74% は ☂ Showers(青)、
75〜89% は ☇ Storm(マゼンタ)、90% 以上は ↯ Compact soon(赤)
- 使用率と、使ったトークン数/ウィンドウ全体(例:"134.4k / 200k")
- 直近12ターンの小さなグラフ(▁▂▃▄▅▆▇█ で描く)
- 直前のターンでどれだけ増えたか(例:"▲ +98.3k last turn")
ターンが終わるたびに更新してください。
claude でセッションを始めてこのプロンプトを貼ると、Claude は一度だけ「このセッションで、保存するとその場で読み込み直す(ホットリロード)機能を有効にしてよいか」を聞いてきます。許可すると、Claude のターンが終わったときにプロンプトの上に帯が現れます。
そこから先は、変更のたびにその場で読み込み直されるので、「Storm は70%から始めて」「最後にドルでのコストも出して」と追加で頼みながら、帯が変わっていく様子をそのまま見られます。
注目したいのは、このプロンプトが 見たいものしか書いていない ことです。API の知識は要りません。どうやって作るか(読み込み直しに耐える状態の置き場所、claude plugin validate でのチェック、どのイベントにフックするか)は、Claude Code に組み込まれている mod の書き方のガイドが受け持っています。先ほどの組み込み mod の表にあった plugin-authoring スキルが、まさにそれです。
📝 この方法で作った mod は、そのセッションでだけ読み込まれ、フォルダも後で片付けられます。残しておきたい場合は、フォルダを別の場所にコピーして、普通のプラグインと同じようにインストールします(手順6で説明します)。
手を動かして理解する:Token Weather を6ステップで作る 🌤️
Claude に任せる前に中身を知っておきたい方や、Claude が書いたコードを確かめたい方のために、ブログは6つの手順で Token Weather を組み立てています。順番に見ていきます。
ステップ1:フォルダを作る 📁
まず、Claude Code のバージョンが新しいことを確かめます。
claude --version # 2.1.287 以降
そのうえで、次の構成を作ります。
token-weather/
├── .claude-plugin/
│ ├── plugin.json
│ └── types/ (mod を読み込むときに Claude Code が書き出す)
├── hooks/
│ ├── hooks.json
│ └── token-weather.mjs
├── types/
│ └── index.d.ts (ステップ3で追加)
└── tests/
└── token-weather.test.ts (ステップ5で追加)
.claude-plugin/plugin.json は、普通のプラグインのマニフェストです。
{
"name": "token-weather",
"version": "0.1.0",
"description": "A live forecast of the context window, drawn above the prompt.",
"author": { "name": "You" }
}
hooks/hooks.json はモジュールを指します。mod が持てるモジュールは1つだけです。
{
"modules": ["./token-weather.mjs"]
}
ステップ2:まず何か描いてみる 🎨
プロンプトのすぐ上の帯は、AbovePrompt という部品です。Claude Code 自身はここに何も描かないので、最初に描く場所としてちょうどいい、とブログは書いています。ui.render イベントにフックして、要素の木を返します。
// hooks/token-weather.mjs
export function register(on) {
on("ui.render", { component: "AbovePrompt" }, ($, e, next) => {
const { Box, Text } = $.ui.resolve(e);
return Box({
paddingX: 1,
children: [Text({ color: "yellow", bold: true, children: "☀ Clear skies" })],
});
});
}
Box や Text はグローバルな変数ではなく、$.ui.resolve(e) から受け取ります。描画先(ターミナルかデスクトップアプリか)ごとに使える部品の種類が少しずつ違うので、いま描こうとしている描画先に合った作り方をその都度もらう、という設計です。JSX も使えて、その場合は h を要素を作る関数として使います。
プラグインを読み込んだ状態でセッションを始めます。
claude --plugin-dir ./token-weather
プロンプトの上に「☀ Clear skies」が出たら成功です 🎉 セッションは開いたままにしておきます。フォルダは見張られていて、保存するたびにモジュールがその場で読み込み直されるので、再起動は要りません。ブログは、この素早いフィードバックの繰り返しこそが mod を書く楽しさの大部分だ、と書いています。
ステップ3:本物の数値を読み、$.state に保存する 📊
$.session.usage() は、ステータスラインと同じ数値を返します。
-
context.tokens:直前の応答が答えるときに使った入力の量 -
context.window:モデルのコンテキストウィンドウの大きさ -
context.percent:前者を後者で割った割合
この呼び出しは無料です。トークン数を数えるリクエストが送られるのは、内訳(breakdown)を求めたときだけです。
数値は、セッションの開始時と、ターンが終わるたびに測ります。
on("session.start", async ($, e, next) => {
const result = await next(e);
await takeReading($);
return result;
});
on("turn.complete", async ($, e, next) => {
const result = await next(e);
if (!e.agentId) {
await takeReading($); // メインのループのターンだけ。サブエージェントは除く
}
return result;
});
どちらのフックも、先に next(e) を呼んでから観察しています。何も変えていない、ということです。
測った値をどこに置くか:変数ではなく $.state に
ここがこのチュートリアルでいちばん大事なところです。モジュールの一番上に let readings = [] と書くのが自然に思えますが、ブログはこれを勧めていません。
読み込み直しは「新しく読み込む」ことなので、register がもう一度動き、session.start ももう一度起き、モジュールの変数は最初からやり直しになります。そこで、履歴は $.state に置きます。$.state は、名前の付いた値を Claude Code 側(ホスト)でセッションの間ずっと持っていてくれるので、読み込み直しても消えません。
// ホスト側が保持するので、このファイルを読み込み直しても履歴は残る
const readings = { plugin: "token-weather", key: "readings" };
async function takeReading($) {
const { context } = await $.session.usage();
if (!context?.window) return;
const tokens = context.tokens ?? 0;
const percent = context.percent ?? Math.round((tokens / context.window) * 100);
const { value: history = [] } = await $.state.get(readings);
await $.state.set(readings, [...history, { tokens, window: context.window, percent }].slice(-HISTORY));
}
(先ほどのドキュメントの例ではモジュールの変数 calls を使っていました。フック同士で共有するだけならそれで十分ですが、読み込み直しのたびに0に戻ってもいいかどうか、で使い分けるとよさそうです。)
$.state に置く値は、プラグインの 型の約束事(type contract)として、小さな .d.ts ファイルで宣言します。types/index.d.ts を追加します。
export type TokenWeatherReading = { tokens: number; window: number; percent: number };
declare module "claude-code" {
interface PluginState {
"token-weather": { readings: TokenWeatherReading[] };
}
}
そして plugin.json に "types": "./types/index.d.ts" を追加します。この手順を飛ばすと、claude plugin validate が直し方まで書いたエラーで止めてくれます。
token-weather.readings is not declared: the manifest's types contract must name it in interface PluginState { … }
この手間の見返りとして、描き直しが自動になります。描画のフックが動いている間に呼んだ $.state.get は、その描画を「購読」した扱いになり、後で $.state.set が呼ばれるたびに帯が描き直されます。$.ui.invalidate を自分で呼ぶ必要はありません。
ステップ4:予報を描く ☀️☁️☂️
ここまでの部品を組み合わせた、モジュール全体です。ブログからそのまま引用します(コメントは日本語にしています)。
// Token Weather:コンテキストウィンドウの天気予報を、プロンプトの上に描く
const HISTORY = 12;
const BARS = "▁▂▃▄▅▆▇█";
const FORECAST = [
{ upTo: 25, icon: "☀", word: "Clear", color: "yellow" },
{ upTo: 50, icon: "☁", word: "Cloudy", color: "cyan" },
{ upTo: 75, icon: "☂", word: "Showers", color: "blue" },
{ upTo: 90, icon: "☇", word: "Storm", color: "magenta" },
{ upTo: Infinity, icon: "↯", word: "Compact soon", color: "red" },
];
// ホスト側が保持するので、このファイルを読み込み直しても履歴は残る
const readings = { plugin: "token-weather", key: "readings" };
export function register(on) {
on("session.start", async ($, e, next) => {
const result = await next(e);
await takeReading($);
return result;
});
on("turn.complete", async ($, e, next) => {
const result = await next(e);
if (!e.agentId) {
await takeReading($); // メインのループのターンだけ。サブエージェントは除く
}
return result;
});
on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
const { value: history = [] } = await $.state.get(readings);
if (e.props.hasSurvey || history.length === 0) {
return next(e);
}
const { Box, Text } = $.ui.resolve(e);
return band(Box, Text, history, e.props.bodyColumns);
});
}
async function takeReading($) {
const { context } = await $.session.usage();
if (!context?.window) return;
const tokens = context.tokens ?? 0;
const percent = context.percent ?? Math.round((tokens / context.window) * 100);
const { value: history = [] } = await $.state.get(readings);
await $.state.set(readings, [...history, { tokens, window: context.window, percent }].slice(-HISTORY));
}
function band(Box, Text, history, columns) {
const now = history[history.length - 1];
const f = FORECAST.find((b) => now.percent < b.upTo);
const parts = [
Text({ color: f.color, bold: true, children: `${f.icon} ${f.word}` }),
Text({ children: ` ${now.percent}% of context` }),
Text({ dimColor: true, children: ` ${short(now.tokens)} / ${short(now.window)}` }),
];
if (columns >= 60) {
parts.push(Text({ dimColor: true, children: " last turns " }));
parts.push(Text({ color: f.color, children: sparkline(history) }));
if (history.length > 1) {
parts.push(Text({ dimColor: true, children: trend(history) }));
}
}
return Box({ flexDirection: "row", paddingX: 1, children: parts });
}
function sparkline(history) {
const top = Math.max(...history.map((r) => r.tokens), 1);
return history.map((r) => BARS[Math.floor((r.tokens / top) * (BARS.length - 1))]).join("");
}
function trend(history) {
const delta = history[history.length - 1].tokens - history[history.length - 2].tokens;
if (delta === 0) return " steady";
return delta > 0 ? ` ▲ +${short(delta)} last turn` : ` ▼ ${short(-delta)} last turn`;
}
function short(n) {
if (n >= 1_000_000) return `${+(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `${+(n / 1_000).toFixed(1)}k`;
return String(n);
}
ブログが「自分の mod にもそのまま持ち帰る価値がある」と挙げている細かい工夫は3つです。
-
部品の props は
e.propsにある:hasSurveyはアンケートがこの帯を使いたがっている合図なので、next(e)で譲っています。bodyColumnsは帯の実際の幅で、会話の横に表示枠が出ているときはターミナルの幅より狭くなります。eの直下にあるのはe.component・e.surface・e.requestId・e.viewportだけです。 -
描くものがないときは譲る:
next(e)を返せば、帯は Claude Code と他の mod に戻ります。 - 絵文字ではなく1文字幅の記号を使う:☀ ☁ ☂ ☇ ↯ は、どのターミナルのフォントでも幅が揃います。
最後の点は、この記事のように絵文字をたくさん使う文章を書いていると、つい逆をやりたくなるところです(ターミナルの表示では、見た目のかわいさより幅が揃うことのほうが大事、ということですね)。
ファイルを保存すると、動いているセッションがそれを拾います。大きなファイルを読むターンを何回か続けると、帯が Clear → Showers → Storm と変わっていきます。ブログのターミナルでの動画では、200k のウィンドウに対して 18% → 67% → 81% と変わる様子が紹介されています。
ステップ5:検証とテスト ✅
claude plugin validate は、Claude Code が読み込むときと同じやり方でマニフェストとモジュールのソースを読み、そのモジュールが何にフックして何を呼ぶかを報告してくれます。
$ claude plugin validate ./token-weather
> types ./types/index.d.ts declares state: token-weather.readings
> ./token-weather.mjs hooks: session.start, turn.complete, ui.render{component=AbovePrompt}
> ./token-weather.mjs calls: $.session.usage (via takeReading), $.state.get, $.state.set (via takeReading), $.ui.resolve
> ./token-weather.mjs state writes: token-weather.readings
> ./token-weather.mjs state reads: token-weather.readings
√ Validation passed
hooks: の行がフックしているイベント、calls: の行が $ を通して呼んでいるものです。この出力は、後で説明する「他人の mod を入れる前に中身を確かめる」ときにもそのまま役立ちます。
claude plugin test は、プラグインの *.test.ts を本物の Claude Code のランタイムで実行します。テストの中で on を使って登録したフックは、鎖の中で mod の 後ろ に入り、Claude Code が返すはずの値の代わりになります。そのため、$.session.usage() が返す値を自由に決められます。ブログのテストの要点だけを抜き出すと、次のようになります。
// tests/token-weather.test.ts(要点の抜粋)
import { describe, expect, test } from "claude-code/testing";
describe("token-weather", () => {
test("the band follows the context window", async ($, on) => {
let tokens = 36_100;
// ここで登録したフックは mod の後ろで動き、Claude Code の答えの代わりになる
on("session.usage", () => ({
value: { startedAt: 0, rateLimits: [], context: { tokens, window: 200_000, percent: Math.round(tokens / 2_000) } },
}));
// ……session.start と turn.complete の代わりの応答を登録し、セッションを始めて
// const ui = await $.ui.mount({ plugin: "token-weather", component: "AbovePrompt", ... }) で帯を描かせる……
expect(await ui.find({ type: "Text", text: /Clear/ })).toBeDefined();
tokens = 134_400;
await $.turn.complete({ reason: "answer", answer: "ok", durationMs: 1 } as any);
expect(await ui.find({ type: "Text", text: /Showers/ })).toBeDefined();
expect(await ui.find({ type: "Text", text: /▲ \+98\.3k last turn/ })).toBeDefined();
});
});
このテストは、ステップ3の「描き直しが自動になる」ことの確認にもなっています。mod は一度も描き直しを頼んでいないのに、turn.complete のあとで帯が Showers に変わっているからです。
ステップ6:共有する 📦
mod はプラグインなので、配り方もプラグインと同じです。いちばん簡単なのは、.claude-plugin/marketplace.json を置いたフォルダを「マーケットプレイス」(プラグインの配布元)にする方法です。
{
"name": "my-mods",
"owner": { "name": "You" },
"plugins": [{ "name": "token-weather", "source": "./token-weather" }]
}
claude plugin marketplace add ./my-mods
claude plugin install token-weather@my-mods --scope user
GitHub のリポジトリにマーケットプレイスのファイルを置けば、そのリポジトリがそのまま配布元になります。受け取る側は、Claude Code の中で3つのコマンドを実行するだけです。
/plugin marketplace add your-org/my-mods
/plugin install token-weather@my-mods
/reload-plugins
更新は普通に push するだけで公開できます。さらに、Claude のディレクトリも mods を含むプラグインを受け付けていて、claude.ai/directory/manage から登録すると、リンクを渡さなくても見つけてもらえるようになります。
もう一歩進んだサンプル:Blast Radius と Replay Theater 🚀
Token Weather は見守って描くだけの mod でした。ブログは続けて、イベントに割り込み、表示枠を開き、入力を受け付ける2つの mod を紹介しています。
| mod | 何をするか | 主なイベント | 描くもの |
|---|---|---|---|
| Token Weather | コンテキストの埋まり具合を天気予報で表示 |
session.start、turn.complete、ui.render
|
プロンプトの上の帯 |
| Blast Radius | 危険なシェルコマンドを止めて、何が変わるかを見せる |
tool.call、ui.render
|
会話の横の表示枠(狭いときは帯) |
| Replay Theater | 直前のターンの編集を1つずつ見返す |
tool.call、turn.start、turn.complete、session.start、command.run、ui.render
|
Prev・Next・Close ボタン付きの表示枠 |
3つとも、anthropics/claude-code-playground リポジトリの claude-code/mods にサンプルとして公開されています。ドキュメントによると、どれも完成したプラグインで、README に作り方が書かれています(サポートなしで、そのままの形で公開されているものです)。
Blast Radius:危険なコマンドの「影響範囲」を実行前に見る 💥
Claude が Bash で rm -rf、git reset --hard、git clean、強制 push、データベースのマイグレーションを呼ぼうとすると、Blast Radius はその呼び出しを止めておきます。そして、そのコマンドが何に触れるのかを調べ、Proceed(進める)と Cancel(やめる)のボタンが付いた表示枠を開きます。2 を押すと Claude には理由付きの拒否が返り、1 を押すと書かれたとおりにコマンドが動きます。
claude.dev の mods ページのデモでは、rm -rf build を止めて、消えることになる 9 個のファイル(1.1 MB)を一覧にしている様子が紹介されています。
中心になっているのは、3つの動きのうちの 応答 です。
on("tool.call", { tool: "Bash" }, async ($, e, next) => {
const risk = classify(String(e.command ?? ""));
if (risk === null) return next(e); // それ以外はいつもどおり動かす
const report = await measure($, risk, await $.session.cwd()); // git status, git clean -n, du など
held = { command: e.command, risk, report, decision: null };
const opened = await $.ui.open({ id: "blast-radius", title: "Blast Radius", focus: true });
if (!opened.isPlaced) held.where = "band"; // 表示枠を置けないほど狭いときは帯に描く
while (held.decision === null && !next.signal.aborted) {
await $.process.run(["sleep", "0.25"]); // $ の呼び出しの中で待つ時間は、フックの持ち時間に数えられない
}
if (held.decision === "proceed") return next(e); // 進める
return { deny: `Blast Radius held this command: the user pressed Cancel. It would have: ${report.summary}.` };
});
流れを図にすると、次のようになります。
ブログが「このサンプルから学べること」として挙げているのは、次の4点です。
-
下調べには
$.process.runを使う:影響の報告は、ツール自身のコマンド(git status --porcelain、git clean -n、git log HEAD..origin/main、showmigrations)から作ります。引数は配列で渡すので、パスに含まれる文字列がシェルのコードとして実行されることはありません。 -
呼び出しを止めておく:フックが1回の呼び出しで使える自分の時間は10秒ですが、
$の呼び出しの中で待っている時間は数えられません(リファレンスによると、$.clock.sleepは例外で数えられます)。そこで、ボタンのonPressが判断を書き込むまで、短いsleepを繰り返して待ちます。Esc を押してnext.signalが中断されたら待つのをやめます。 -
ホットキー付きのボタン:
Button({ label: "Proceed", hotkey: "1", onPress })は、クリック、Tab と Enter、数字キーのどれでも押せます。 -
狭いときは帯に切り替える:ターミナルの幅が十分あれば、表示枠は会話の横に置かれます。
$.ui.openがisPlaced: falseを返したら、同じ報告をプロンプトの上の帯に描きます。
そして、ブログはこの mod の限界もはっきり書いています。
It's a safety net, not a permission system.
(これは安全網であって、権限の仕組みではありません。)
Blast Radius はコマンドの文字列を読んでいるだけなので、$(…) やエイリアス、rm を呼ぶスクリプトはすり抜けます。本当に止めたい操作は、パーミッションのルールで止めるべきです。
Replay Theater:直前のターンの編集を1つずつ見返す 🎬
Replay Theater は、ターンの間に行われたすべての Edit と Write の呼び出しを記録します。記録するのは、ファイル名と、変更前と変更後のテキストです。ターンが終わるとプロンプトの上にヒントが出て、r を押す(または /replay と入力する)と、表示枠が編集を1つずつ差分で見せてくれます。番号付きのステップの並びと、Prev・Next・Close のボタンが付いています。
Replay Theater は編集を止めたり変えたりは一切せず、観察 だけをしています。
on("tool.call", async ($, e, next) => {
if (EDIT_TOOLS.has(e.tool)) state.pending.push(...(await stepsFor($, e))); // 変更前後のテキスト → 差分
return next(e); // 編集自体はそのまま動かす
});
on("turn.start", ($, e, next) => { if (!e.agentId) state.pending = []; return next(e); });
on("turn.complete", async ($, e, next) => {
const r = await next(e);
if (!e.agentId && state.pending.length) state.replay = state.pending; // 1ターンにつき1つのリプレイ
return r;
});
on("session.start", async ($, e, next) => {
const r = await next(e);
await $.command.register({ name: "replay", description: "Step through the last turn's file edits" });
return r;
});
on("command.run", { command: "replay" }, async ($, e) => ({ text: (await openReplay($)) ? "Replaying" : "No edits" }));
イベントのつながりを図にすると、次のようになります。
このサンプルから学べることとして、ブログは次の4点を挙げています。
-
イベントを対にする:
turn.startとturn.completeで編集を挟み、1ターンにつき1つのリプレイにまとめます。e.agentIdを見て、サブエージェントのターンは混ぜないようにしています。 -
スラッシュコマンドを登録する:
session.startで$.command.registerを呼び、command.runでそれに答えます。 -
ファイルを読む:Write の場合は、書き込みが行われる直前に
$.fs.readで元の内容を取っておくので、差分が本物になります。 - 置き場所は描く場所の仕事:全画面では表示枠が右に置かれ、80桁のときはプロンプトの上にその場で開きます。mod はどちらでも同じ木を描くだけです。
コミュニティの mod も出てきている 🌱
claude.dev の mods ページには、コミュニティの mod として Next steps(作者:Thariq Shihipar さん)も紹介されています。ターンが終わると次に送るプロンプトの候補を最大3つ出してくれて、1・2・3 を押すとそのプロンプトが入力欄に下書きされ、0 で閉じられます。ページにはインストールのコマンドが1行だけ載っていますが、コミュニティのマーケットプレイスは既定では登録されていないので、ドキュメントに従って先に追加しておきます。
claude plugin marketplace add anthropics/claude-plugins-community
claude plugin install next-steps@claude-community
ソースは anthropics/claude-plugins-community の next-steps で読めます。
安全に使うために 🔐
ここまで見てきたとおり、mods はかなり強力です。強力だということは、その分だけ注意も必要だということです。ドキュメントは、インストールの説明の冒頭に次の警告を置いています。
A mod is code that runs with your permissions. It can read and write your files, start processes, and make network requests. Install mods only from authors and marketplaces you trust.
(mod は、あなたの権限で動くコードです。ファイルを読み書きし、プロセスを起動し、ネットワークにリクエストを送れます。mod は、信頼できる作者とマーケットプレイスからだけインストールしてください。)
mod が手を伸ばせる範囲
ドキュメントの「What a mod can reach」によると、読み込まれた mod は次のことができます。
| 範囲 | 内容 |
|---|---|
| あなたとしてマシン上で動く | ユーザーアカウントで届くどこのファイルでも読み書きし、プログラムを起動し、ネットワークにリクエストを送る |
| 秘密情報を読む | 環境変数や設定ファイル。そこに置いた API キーも含む |
| セッションを見る | 送ったすべてのプロンプトと、Claude のすべてのツール呼び出し |
| セッションを変える | プロンプトやツール呼び出しを書き換える、あなたが打ったかのようにプロンプトを送る、別のセッションにメッセージを送る |
| 確認なしで動く | 本来なら確認されるツール呼び出しを、確認の前に承認する |
| 利用枠を使う | あなたのプランや API キーでモデルを呼ぶ |
さらに、ドキュメントは次の2点を明記しています。
- mod はサンドボックスに入っていません。 サンドボックス機能を有効にしても、それが隔離するのは Claude が実行する Bash コマンドで、mod が起動したプロセスはその外で動きます。
-
ツール呼び出しを承認する mod は、
askルールで確認されるはずの呼び出しや、自分のPreToolUseフックが止めた呼び出しも承認できます。denyルールが拒否する呼び出しまで承認できる場合の条件は、パーミッションのドキュメントにまとめられています。
一方で、変えられないものもあります。mod は Claude Code の表示の大部分を描き直せますが、パーミッションの確認画面は変えられません。確認画面に表示される内容を mod が書き換えることはできない、ということです。
インストールする前に、中身を一覧にする 🔍
先ほどの claude plugin validate が、ここで役に立ちます。プラグインのファイルを手元に持ってくれば(リポジトリをクローンするなど)、実行せずに、その mod がどのイベントを扱い、何を Claude Code に頼むのか(ファイルを読む、ネットワークにリクエストを送る、など)を一覧にできます。
claude plugin validate ./some-mod
出力の hooks: と calls: の行を見ます。
たとえば「プロンプトの上に天気を出すだけ」のはずの mod が、calls: にネットワークのリクエストを並べていたら、立ち止まって理由を確かめる価値があります。外とのやり取りがすべて $ を通る設計なので、こうした一覧が作れるわけです。ブログも、mod はパッケージを入れるときと同じように扱い、先にリポジトリを読んで、信頼できる人からだけ入れるよう勧めています。
mods を止める方法 🧯
うまく動かないときや、一時的に切り分けたいときのために、止め方も整理しておきます。ドキュメントによると、どの範囲をどれくらいの期間止めるかで方法が分かれます。
| 止めたい範囲 | 方法 |
|---|---|
| 1つの mod だけ |
/plugin の Installed タブで、そのプラグインを無効化またはアンインストールする |
| 入れたすべての mod を、1セッションだけ |
--safe-mode を付けて Claude Code を起動する(他のカスタマイズも止まる) |
| 自分で入れたすべての mod を、すべてのセッションで |
~/.claude/settings.json に "disableAllHooks": true を設定する(設定ファイルのフックとカスタムのステータスラインも止まる。組織が管理しているものは動き続ける) |
注意点として、disableAllHooks、--bare、--safe-mode といった設定やフラグは、組み込みの mod は止めません。組み込みの mod は、基本的に /plugin で個別に無効化します(cc-plugin-sec-default だけは利用者が止めることはできず、管理者が managed settings で順序を決めます)。
セッションでどの mod が読み込まれているかは、プロンプトで /plugin を開くと、タブの下に 1 mod active · first-mod のような薄い行で表示されます(組み込みの mod はこの行には出ません)。
組織で Claude Code を使っている場合は、管理者が managed settings を通して、mods を動かすかどうかや、どの mod を動かすかを決められます。ユーザーが入れた mod を止める設定(allowManagedModsOnly など)や、mod の中身のレビュー方法、ポリシーを強制する mod の作り方は、ドキュメントの「Manage mods for your organization」にまとまっています。
覚えておきたい4つの習慣 📝
ブログの終盤には、mod を書くときに身に付けておきたい習慣が4つまとめられています。
| 習慣 | 内容 |
|---|---|
| Claude Code が書き出す型定義に頼る | mod を読み込むたびに、Claude Code はそのビルド用の型宣言を .claude-plugin/types/ に書き出す。エディタや tsc -p がそのまま使え、すべてのイベント・$ のメソッド・部品の props の正式な参照先になる |
props は e.props から読む |
hasSurvey や bodyColumns などは e の直下ではなく e.props にある |
| 読み込み直しを前提にする | 保存するたびに register と session.start がもう一度動くので、データはモジュールの変数ではなく $.state に置く |
| 描いたものが出ないときはログを読む |
claude --debug で起動し、フックが検証に通らない木を返した、という行を探す |
1つ目の型定義は、冒頭の注意書きとつながっています。API はリリースごとに変わる可能性があるので、ブログやこの記事のコードよりも、手元の .claude-plugin/types/ に書き出された型のほうが、その時点のバージョンにとっての正解になります。
何を作るか:アイデアの出発点 💡
ブログは最後に、3つのサンプルがそれぞれ1つの問いから生まれたことを紹介しています。
- Token Weather:「コンテキストはどれくらい埋まっている?」
- Blast Radius:「このコマンドは何を消そうとしている?」
- Replay Theater:「Claude はいま何を変えた?」
そして、自分の問いは人それぞれ違っていて、それこそが大事なのだと続けます。出発点として挙げられているアイデアは次のとおりです。
| アイデア | 使う API・イベント(3行目と5行目は筆者の補足) |
|---|---|
| コストやレート制限のメーターをステータスラインに出す |
$.session.usage()、$.ui.status
|
| チームの約束事を、毎回のプロンプトに自動で足す |
prompt.submit のフック |
| このセッションで Claude が読んだファイルを並べる表示枠(見たものの地図) |
tool.call の観察と表示枠 |
| 長いターンが終わったら知らせる集中タイマー | $.ui.toast |
自分のスタックに合わせたガード(本番の kubectl のコンテキストや terraform apply など) |
tool.call の応答 |
個人的には、最後の「自分のスタックに合わせたガード」が、多くのチームにとっていちばん手を付けやすいと感じました。ただし Blast Radius のところで見たとおり、これは安全網です。本当に止めたい操作はパーミッションのルールで止めたうえで、「止まったときに何が起きるかを見せる」役割を mod に任せる、という組み合わせがよさそうです。
まとめ
この記事では、Claude Code の mods について、公式ブログ・ドキュメント・claude.dev の mods ページをもとに整理しました。
- mods は、Claude Code の中で動く JavaScript / TypeScript のフック で、プラグインとして配布されます。設定ファイルのフックやスキル、MCP サーバーと違い、画面に独自の表示を描けて、イベントを書き換えたり自分で答えたりできます。
-
フックは鎖の形 で、
next(e)でイベントを受け渡します。できる動きは、観察・書き換え・応答の3つです。 -
Token Weather は、
$.session.usage()で数値を読み、$.stateに履歴を置き、AbovePromptに描く、約80行の mod です。$.stateを使うと読み込み直しに強くなり、描き直しも自動になります。 - Blast Radius と Replay Theater からは、ツール呼び出しを止めておく方法、表示枠とボタン、スラッシュコマンドの登録、イベントを対にする書き方が学べます。
-
mod はあなたの権限で動くコード です。入れる前に
claude plugin validateで中身を一覧にし、信頼できる作者からだけ入れます。
それでも、この記事でいちばん持ち帰ってほしいのは、「欲しい表示を文章で説明すれば、Claude が mod を書いてくれる」 という入口の軽さです。API を覚えるところから始めなくても、「いつもこれが見えていたらいいのに」という小さな不満を1つ選んで、Claude に頼んでみるところから始められます 🙌
次の一歩としては、まず claude --version で v2.1.287 以降になっていることを確かめ、Token Weather のプロンプトを貼ってみるのがおすすめです。帯が出たら、色やしきい値を自分好みに変えてもらうだけでも、mods の動き方がよくわかります。本記事では詳しく扱わなかった表示枠の作り方(タブ、テキスト入力)、モデルの呼び出し、タイマー、組織での管理については、公式ドキュメントの各ページにまとまっています。
参考
- Getting started with Claude Code mods — claude.dev Blog — 本記事の主な元記事。Token Weather の6ステップ、Blast Radius、Replay Theater、4つの習慣(Addy Osmani、2026-10-01)
- Mods overview — Claude Code Docs — mod にできること、権限、動く場所、止め方、他の仕組みとの比較、組み込みの mod の一覧
- claude.dev/mods — 3つのサンプルのデモ、組み込みの mod、コミュニティの mod(Next steps)の紹介ページ
-
anthropics/claude-code — mods —
/diff・AGENTS.md・sec-default・telemetryなど、組み込み mod のソースとテスト - anthropics/claude-code-playground — claude-code/mods — Token Weather・Blast Radius・Replay Theater のサンプル
- anthropics/claude-plugins-community — next-steps — コミュニティの mod「Next steps」のソース
- Manage mods for your organization — Claude Code Docs — 管理者向けの既定の挙動、managed settings、mod のレビュー、ポリシー用の mod

