Claude Codeを独自にカスタマイズ可能に:UIの変更、危険なコマンドの阻止、コード変更のステップ確認

コンテキスト使用量をプロンプト入力欄の上に常時表示し、危険なコマンドの実行前に影響を確認し、一連のコード変更をステップごとにレビューする——3つの公式サンプルでModsの役割と書き方を解説。

著者:Anthropic / Addy Osmani10-02 · 約15分
ClaudeDevs公式発表動画:ModsがClaude Codeのインターフェースとワークフローをどのように変えるかをご覧ください。

ターミナルでClaude Codeを使ってプログラミングを行う際、開発者からはよく次のような3つの具体的なニーズが挙げられます:

  • 重要な数値を常時確認したい:ステータスバーをわざわざ確認しなくても、プロンプト入力欄の上に数値を常時表示させ、現在のコンテキスト使用量を直感的に把握したい。
  • 不安なコマンドを事前にブロックしたい:クリーンアップやリセット、マイグレーションなどの高リスクなコマンドを実行する前に、具体的にどのファイルに影響が及ぶかを確認した上で、実行を許可するかどうか判断したい。
  • 自分のペースで変更内容をレビューしたい:1回のやり取りで複数のファイルが編集された際、すべての編集が終わってから散乱したGit diffを探すのではなく、各変更の前後の差分を1つずつステップ確認したい。

これまでもClaude Codeは、設定(settings)、権限ルール(permission rules)、スラッシュコマンド(slash commands)、スキル(skills)、ステータス行を通じて動作の調整をサポートしていました。バージョン 2.1.287 以降 でデフォルト有効化された Mods は、さらに一歩進んだ機能です。JavaScriptやTypeScriptモジュールを用いて一部の動作を書き換えたり置き換えたりできるほか、独自のカスタムUIを描画することも可能になります。

自分が必要な機能をいくつか選んでClaude Codeに追加するようなものだと捉えてください。欲しい機能があれば、Claudeに直接伝えるだけで作ることができます:

  • 入力欄の上にコンテキスト使用量行を追加:例えば、「プロンプト入力欄の上にコンテキスト使用量を表示する行を追加し、上限に近づいたら色を変え、直近の変化も表示してほしい」と伝えます。ClaudeがこのModを生成し、現在のセッションでホットリロード(コードを保存するだけで即座に反映)を許可すると、入力欄の上に毎ターン更新される使用量バーが現れます。続けて「ストーム(嵐)のしきい値を75%から70%に変えて」と指示してコードを保存すれば、新しいしきい値が即座に適用されます。
  • ファイル削除前に影響範囲を確認:Blast Radiusを導入すると、build ディレクトリの削除を実行する前に専用パネルが開き、削除されるファイルの一覧と占有容量が表示され、Proceed(続行)かCancel(キャンセル)かを数字キーで選択するまで待機します。
  • 変更完了後にステップごとに差分を確認:Replay Theaterを導入すると、1ターンのやり取りで複数のファイルが編集された後、入力欄の上にリプレイ案内が表示されます。r キーを押すか /replay と入力するだけで、サイドパネルで各編集の前後の差分(diff)を1ステップずつ確認できます。確認後にパネルを閉じれば、ファイルは変更後の状態のまま維持されます。

低レイヤーの仕組みとして、Modは本質的にプラグイン(Plugin)内に含まれるフック(Hook)であり、セッション内でモジュールとして常駐動作しながら各種イベントを受信します。Anthropic公式の機能の一部(サイドバーの /diff パネルや AGENTS.md 仕様のサポートなど)も、Modの形式で直接実装されています。例えば、内蔵の /diff 機能は現在Modとなっており、ユーザーは /plugin で無効化したり、自作のカスタム版に置き換えたりできます。公式は今後、さらに多くの内蔵機能をModへと移行させ、ユーザーが必要に応じてコア機能を軽量化したり拡張したりできるようにする計画です。

公式チュートリアルでは、約80行のコードでコンテキスト使用量バーを作成し、さらにコマンド遮断と変更リプレイという2つの実践例を紹介しています。これらのサンプルを理解すれば、自作ツールがどのイベントをフックし、どのような状態を保持すべきかが分かります。

一、 コア概念:Modとは何か?従来のHooksと何が本質的に違うのか?

一言でまとめると:Modの本質は、Plugin内にパッケージされたJavaScript/TypeScriptモジュールであり、セッション内に常駐してClaude Codeが提供するイベントを受け取り、入力を監視・書き換えたり、結果を直接返したりする仕組みです。

1. どのように動作するのか?

ファイル構造の面では、Modは標準のClaude Code Plugin仕様に準拠しています:

  • ルートディレクトリに .claude-plugin/plugin.json マニフェストファイルが存在する。
  • hooks/hooks.json 内の modules フィールドで具体的なJS/TSファイルを指定する。
  • 該当モジュールは register(on, options) 関数をエクスポートする。
  • 関数内部で on(event, matcher?, hook) を呼び出して各種フックを登録する。

各フックの基本的なコード構造は次のようになります:

on("tool.call", { tool: "Bash" }, async ($, e, next) => {
  // $    Mods APIを表す:ui、session、state、store、fs、process、clockなどのシステム機能を提供
  // e    今回のイベントの入力データ(プレーンなデータオブジェクト)
  // next イベントを次のプラグインに渡し、最終的にClaude Code本体へと渡す
  return next(e);
});

2. イベントの監視(Observe)、書き換え(Rewrite)、直接応答(Answer)

イベントが発生したとき(例えばClaudeがBashを呼び出してコマンドを実行しようとするとき)、ExpressやKoaのミドルウェアのように、イベントはMod、他のプラグインを順に通り、最終的にClaude Codeのデフォルトの実行処理に到達します。

このチェーンの中で、Modには大きく分けて3つの制御アプローチがあります:

Observe(監視):元の動作を維持する。 対話ターンの終了後に入力トークン数を読み取ったり、Claudeが変更したファイルを記録したりします。

const r = await next(e); // 先に後続のチェーンを実行させる
// 結果を検査
return r;               // そのまま返す

Rewrite(書き換え):入力を変更してから後続へ渡す。 コマンドの引数を安全なものに置き換えたり、プロンプト送信時にチームのコーディング規約を付加したりします。後続のプラグインとClaude Codeには変更後のデータが渡されます。

return next({ ...e, command: saferCommand });

Answer(直接応答):独自の結果を直接返す。 ツールの呼び出しを拒否したり、スラッシュコマンドを処理したりします。nextを呼び出さないため、イベントはそれ以降の後続チェーンには伝達されません。

return { deny: "拒绝原因" }; // next を呼び出さない
1つのイベントに対する3つの処理アプローチ

イベントチェーンの概念図。アプローチを切り替えて、イベントが後続に伝達されるか確認できます(ここでは実際のコマンドは実行されません)。

  1. ツール呼び出しイベント元の入力 e
  2. 自作の Modawait next(e)、結果を待機
  3. 他のプラグイン元の入力 e を受信
  4. Claude Code元の動作を実行し、結果をチェーン沿いに返却

監視:イベントはそのまま後続へ伝達され、Modは結果を読み取った後にそのまま返し、元の動作を変更しません。

書き換え:next(変更後の入力) を呼び出し、後続プラグインとClaude Codeが新しい入力を受け取ります。直接応答:結果を直接返しnextを呼ばないため、後続チェーンは実行されません。

3. ModとSettings Hooksの違い

Settings hookは各イベントの発生時にシェルコマンドを1回実行し、標準入力(stdin)と標準出力(stdout)経由でJSONデータを受け渡します。

一方、Modは起動時に一度ロードされ、セッション中に常駐し続けます:

  • 状態の保持:セッション全体を通じて内部状態を保持できます。
  • UIとインタラクションの描画:イベントに応じてリアルタイムにUIを描画・更新でき、ターミナル環境とデスクトップアプリの双方をサポートします。カスタムパネルや通知を表示できるだけでなく、ボタンや入力要素を追加して他のModがその入力操作に応答することも可能です。
  • イベントの介入と書き換え:モデルにプロンプトが送信される前に内容を書き換えたり、ツールの呼び出しをインターセプトして再試行させたり、パーミッション要求を承認または拒否したり、モデルがツール出力を読み取る前にAPIキーなどの機密情報を能動的に秘匿化したりできます(フィルタリングルールの実装が必要です)。
  • ホスト機能の呼び出し:サイドパネル(Pane)のオープン、プロセスの実行、新しいスラッシュコマンドの登録、モデルが呼び出せるカスタムツールの登録など、Claude Codeのホスト機能を直接呼び出せます。

Modの実行環境にはブラウザのDOMもNode.jsのグローバルオブジェクトも存在せず、外部とのやり取りはすべて $ オブジェクトが提供するAPIを通じて行われます。ただし、この実行環境の分離はModのマシンアクセスを制限するものではなく、ModはClaude Codeと完全に同じローカルシステムの権限を保持しています。

二、 80行の実践コード:「Token Weather(トークン天気予報)」をゼロから作る

公式チュートリアルでは、Token Weather というModをゼロから実装しています。各対話ターンの終了後にコンテキストウィンドウの使用状況を読み取り、プロンプト入力欄の上に1行の情報を描画します。ここには天気アイコン、使用率(パーセンテージ)、ウィンドウ上限に対する現在のトークン比率、直近ターンのミニ推移チャート(スパークライン)、そして直前ターンで増加したトークン数が表示されます。

☀ Clear   18% of context    36.1k / 200k     last turns ▁▂▃▄   ▲ +36.1k last turn

天気アイコンとテキストは、現在の使用率に応じて以下のようにマッピングされます:

  • 25%未満:☀ Clear
  • 25%–49%:☁ Cloudy
  • 50%–74%:☂ Showers
  • 75%–89%:☇ Storm
  • 90%以上:↯ Compact soon
ターミナルセッションにおけるToken Weatherの動作の様子:読み込むファイルが増えるにつれて、コンテキスト使用量がClear(18%)からShowers(67%)、Storm(81%)へと上昇していく。

まずは試してみる:要件を伝えてClaudeに書いてもらう

Claude CodeにはMod作成用のガイドが組み込まれており、要件を伝えるだけでプラグインを生成してくれます。ターミナルで claude を起動し、以下のプロンプトをそのまま貼り付けてください:

Make me a Claude Code mod called token-weather: a live forecast of my context window, shown in the band above the prompt.

What it should show, on one line:
- A weather icon and word for how full the context window is: under 25% ☀ Clear (yellow), 25–49% ☁ Cloudy (cyan), 50–74% ☂ Showers (blue), 75–89% ☇ Storm (magenta), 90% and up ↯ Compact soon (red).
- The percentage used, then the tokens used out of the window, like "134.4k / 200k".
- A small chart of the last 12 turns, drawn with ▁▂▃▄▅▆▇█.
- How much the last turn added, like "▲ +98.3k last turn".

It should update after every turn.

Claudeが現在のセッションでホットリロード(Hot Reloading)を有効にするかどうか尋ねてきます。許可すると、そのターンが完了した時点で入力欄の上に天気バーが表示されます。以降はコードを修正して保存するたびにその場でリロードされるため、Claudeに「ストームのしきい値を70%にして」「末尾にドル換算の費用を追加して」と微調整を依頼し、リアルタイムで変化を確認できます。このショートカットで作成されたModは現在のセッションでのみ読み込まれ、そのディレクトリは後にシステムによってクリーンアップされます。恒久的に使い続けたい場合は、生成されたディレクトリをコピーして退避し、ステップ6の手順に従ってインストールしてください。

その構造を深く理解したい場合や、自ら手作業でより高度な機能をカスタマイズしたい場合は、内部実装をステップごとに分解して見ていきましょう。

ステップ 1:プラグインのディレクトリ構造を準備する

まず、ローカルのClaude Codeのバージョンが要件を満たしているか確認します:

claude --version   # 2.1.287 以降である必要があります

続いて、次のような標準ディレクトリを構築します:

token-weather/
├── .claude-plugin/
│   ├── plugin.json
│   └── types/            (ロード時にClaude Codeが現在のバージョンの型定義を書き込む)
├── hooks/
│   ├── hooks.json
│   └── token-weather.mjs
├── types/
│   └── index.d.ts        (ステップ3でプラグインの状態の型を宣言する)
└── tests/
    └── token-weather.test.ts

.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 は、フックスクリプトがどこにあるかをClaude Codeに伝えます(1つのModにつき1つのエントリモジュール):

{
  "modules": ["./token-weather.mjs"]
}

ステップ 2:画面上に最初のテキストを描画する(UIマウントとホットリロード)

Claude Codeのインターフェースは複数のコンポーネント領域に分割されています。プロンプト入力欄のすぐ上には AbovePrompt と呼ばれる空き領域があります。デフォルトでは公式はこの領域に何も描画しないため、常駐ステータスバーを配置するのにうってつけです。

hooks/token-weather.mjs で ui.render イベントをフックします:

// hooks/token-weather.mjs
export function register(on) {
  on("ui.render", { component: "AbovePrompt" }, ($, e, next) => {
    // UIコンポーネントはグローバル変数を使用できないため、$.ui.resolve(e) を通じて現在のレンダリング環境に応じたコンストラクタを解決する必要がある
    const { Box, Text } = $.ui.resolve(e);
    return Box({
      paddingX: 1,
      children: [Text({ color: "yellow", bold: true, children: "☀  Clear skies" })],
    });
  });
}

注意:Modはサンドボックス内で動作し、Box や Text などのコンポーネントはグローバルに利用できるオブジェクトではありません。Claude Codeは純粋なターミナル文字列インターフェースで動作することもあれば、デスクトップGUIクライアントで動作することもあり、レンダリングホストによってサポートされるコンポーネントがわずかに異なるためです。そのため、必ず $.ui.resolve(e) を通じて現在の環境に応じたコンポーネントファクトリを動的に取得する必要があります。

このプラグインを実行してテストします:

claude --plugin-dir ./token-weather

ターミナルの入力欄の上に、黄色の「☀ Clear skies」が表示されます。セッションを開いたまま token-weather.mjs を編集して保存すると、ディレクトリが監視されているため、セッションを再起動することなくコードがその場でリロードされます。

ステップ 3:実際のデータを収集し状態を永続化する($.state とリアクティブレンダリング)

UIの骨組みができたので、次は実際のトークン使用量を取得します。

$.session.usage() を呼び出すことで、ステータスバーと完全に一致する統計情報を取得できます:

  • context.tokens:直前のターンの回答のベースとなった入力トークン数
  • context.window:モデルのコンテキストウィンドウ上限(例:200k)
  • context.percent:両者を除算して得られる使用率(パーセンテージ)

この呼び出しのオーバーヘッドは非常に小さく、詳細な内訳(breakdown)を明示的に要求しない限り、リモートへの追加のトークン計算リクエストは発生しません。

セッション起動直後(session.start)および各対話ターンの完了時(turn.complete)にデータを記録します:

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($); // 重要なポイント:メインセッションのみを集計し、サブAgentによる内部ターンは無視する
  }
  return result;
});

なぜ通常のモジュール変数に履歴を保存してはいけないのか?

モジュールのトップレベル変数(let readings = [] など)を定義するのは一見自然に思えますが、ホットリロード時にデータを保持できません。ホットリロードは完全に新規のロード処理であり、register が再実行され、session.start も再び発火するため、モジュールスコープの変数は再初期化されてしまいます。

正しいアプローチは、履歴レコードを $.state に保存することです。ホスト側がセッション中に一元管理するため、ホットリロードをまたいでもデータが失われません。

// ホスト内に保持する状態キーを宣言
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);
  
  // $.state から過去の履歴を取得し、新しいレコードを追加して直近12ターン分を保持
  const { value: history = [] } = await $.state.get(readings);
  await $.state.set(readings, [...history, { tokens, window: context.window, percent }].slice(-HISTORY));
}

型定義コントラクトと自動再描画メカニズム

$.state を使用する場合、Claude Codeはプラグインの型定義コントラクトで明示的な宣言を行うことを求めています。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 の実行時に型定義コントラクトの宣言不足が警告されます。

ui.render フックの実行中に $.state.get を呼び出すと、ホスト側はその状態に対するサブスクリプションを自動的に確立します。その後、$.state.set を呼び出して状態を更新するだけで、$.ui.invalidate を手動で呼び出すことなく、ステータスバーが自動的に再描画されます。

ステップ 4:全体の実装コード——天候マッピング、スパークライン、トレンド表示

それでは、すべてのロジックを統合します。以下は hooks/token-weather.mjs の完全なソースコードです:

// 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)}` }),
  ];
  
  // レスポンシブ対応:ターミナルの利用可能幅が60列以上の場合にのみスパークラインと増加トレンドを表示
  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 });
}

// Unicodeの単一文字の高さブロックを組み合わせて直近12ターンの小型推移グラフを作成
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);
}

このコードから学ぶべき3つの実装の工夫:

  1. e.props からコンポーネントプロパティを取得する: e.props.hasSurvey は公式のアンケートがそのスロットを使用しているかどうかを示します。使用中なら return next(e) を実行して表示領域を譲ります。e.props.bodyColumns は、現在のターミナルのその行における実際の利用可能幅を表します(例えば横にdiffパネルが開いている場合、利用可能幅は狭くなります)。
  2. 何も描画しないときは必ず return next(e) を呼び出す: null や空のノードを返さず、next(e) を呼び出すことで他のプラグインや公式のデフォルト動作に制御を返します。
  3. Emojiを避け、単一幅の文字を一貫して使用する: コード内では ☀ ☁ ☂ ☇ ↯ と ▁▂▃▄▅▆▇█ を使用しています。ターミナル環境では、複雑なEmojiは2文字幅扱いになったりグリフが途切れたりカーソル位置がズレたりする不具合が起きやすいのに対し、標準的な単一幅の記号文字であればすべてのターミナルの等幅フォントで綺麗に揃います。

ステップ 5:プラグインのバリデーションと自動テスト

Claude Codeは、宣言、呼び出し、および実際の挙動を検査するためのプラグイン検証コマンドとテストコマンドを提供しています。

静的検証:claude plugin validate

claude plugin validate ./token-weather を実行すると、モジュールが静的にスキャンされます:

$ 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

どのイベントを購読しているか、どのAPIを呼び出しているか、どの状態を読み書きしているか、そして状態がTypeScriptの型コントラクトで宣言されているかが明確にリストされます。

単体テスト:claude plugin test

Modは自動テストを記述できます。テストスクリプトは本物のClaude Codeランタイム環境で直接実行されます。テスト内で on を使って登録したフックは自動的に自作Modの後ろに配置されるため、公式インターフェースの戻り値をモック(Mock)するのに最適です:

// tests/token-weather.test.ts
import { describe, expect, test } from "claude-code/testing";

describe("token-weather", () => {
  test("天气条能够随上下文增长正确切换", async ($, on) => {
    let tokens = 36_100;
    // セッション環境とトークン使用量をモック
    on("session.start", ($, e) => ({ cwd: e.cwd }));
    on("session.usage", () => ({
      value: { startedAt: 0, rateLimits: [], context: { tokens, window: 200_000, percent: Math.round(tokens / 2_000) } },
    }));
    on("turn.complete", () => ({ text: "" }));

    await $.session.start({ surface: "terminal", isInteractive: true, cwd: "/work" } as any);
    const ui = await $.ui.mount({
      plugin: "token-weather",
      surface: "terminal",
      component: "AbovePrompt",
      props: { hasSurvey: false, isWorking: false, maxRows: 10, bodyColumns: 120 },
    } as any);

    // 初期状態:36.1k / 200k (18%)、Clearが表示されるはず
    expect(await ui.find({ type: "Text", text: /Clear/ })).toBeDefined();

    // 第2ターンをシミュレート:トークンが134.4k (67%) に増加
    tokens = 134_400;
    await $.turn.complete({ reason: "answer", answer: "ok", durationMs: 1 } as any);

    // 手動でinvalidateをトリガーしなくてもUIが自動的にリアクティブ更新されることを検証
    expect(await ui.find({ type: "Text", text: /Showers/ })).toBeDefined();
    expect(await ui.find({ type: "Text", text: /67% of context/ })).toBeDefined();
    expect(await ui.find({ type: "Text", text: /▲ \+98\.3k last turn/ })).toBeDefined();
    
    await ui.unmount();
  });
});

公式のサンプルテストでは、claude plugin test ./token-weather によって、プロンプト入力欄の上のステータス行がトークンの増加とターンの完了に応じてテキストを正しく更新することが検証されます。また、ステップ3で説明した自動再描画メカニズムも検証されます。すなわち、turn.complete が発火した後、Mod側で再描画を手動要求することなく画面更新が完了します。

ステップ 6:共有とインストール

Mod自体が標準のClaude Codeプラグインであるため、配布方法は完全に同一です。.claude-plugin/marketplace.json を含むディレクトリまたはリポジトリを作成してマーケットプレイスとして機能させることができます:

{
  "name": "my-mods",
  "owner": { "name": "You" },
  "plugins": [{ "name": "token-weather", "source": "./token-weather" }]
}

ローカルのプラグインディレクトリに対しては、CLIコマンドを使ってマーケットプレイスを追加してインストールできます:

claude plugin marketplace add ./my-mods
claude plugin install token-weather@my-mods --scope user

Modとマーケットプレイス設定をGitHubリポジトリに配置している場合、他のユーザーはClaude Codeセッション内で3つのコマンドを実行するだけでインストールできます:

/plugin marketplace add your-org/my-mods
/plugin install token-weather@my-mods
/reload-plugins

リロードを実行するとModが反映されます。正常に表示されない場合は、Claude Codeを再起動してください。

Mods APIはバージョンアップに伴い変更される可能性があります。Claude CodeがModをロードするたびに、.claude-plugin/types/ に現在のバージョンに対応する型定義ファイルが書き込まれるため、ローカルでのコーディングと検証はこの型宣言に準拠して行ってください。また、Modを含むプラグインは公式のプラグインディレクトリ(claude.ai/directory/manage)に提出することも可能です。

三、 応用解説:公式の2つの高度なModはどのように活用できるか?

Token Weatherは「読み取りと描画のみ」を行う軽量なModでした。公式チュートリアルでは続いて、さらに強力な2つのModを取り上げ、イベントのインターセプト、ダイアログ操作、複雑なデータフローの処理を網羅的に解説しています。

ケース 1:Blast Radius(高リスクコマンドの事前影響プレビューとブロック)

ClaudeがBashを呼び出して rm -rf、git reset --hard、git clean、強制プッシュ(force push)、またはデータベースマイグレーション(database migration)を実行しようとすると、Blast Radiusはその呼び出しを一時停止します。まずコマンドが具体的にどのファイルに影響するかを分析し、Proceed と Cancel の選択肢を含む確認パネルを開きます。2 を押すとClaudeに拒否理由が返され、1 を押すとコマンドの実行が許可されます。

Blast Radiusターミナルデモ:rm -rf buildをインターセプトし、削除対象の9ファイル(1.1 MB)を一覧表示。Cancelを選択して実行を拒否し、2回目の試行時にProceedを選択して実行を許可する様子。

このModは3つのフックを登録しています。Bashに対する tool.call、および Pane と AbovePrompt に対する ui.render です。その中核ロジックは、前述の「Answer(直接応答/乗っ取り)」メカニズムに属します:

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()); // dry-run: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}.` };
});

中核メカニズムと設計上の考慮点:

  1. 引数配列による $.process.run でドライラン(dry-run)を実行: 影響レポートはツール自身が提供するコマンド(git status --porcelain、git clean -n、git log HEAD..origin/main、showmigrations など)から取得されます。引数はargv配列として渡されるため、パス内の文字列がシェルスクリプトとして解釈される危険はありません。
  2. 待機時間の計算方法: フックのディスパッチ自体には10秒の実行制限時間がありますが、$ API呼び出しの内部で待機している時間(ここでの sleep プロセスなど)はフック自身の制限時間にはカウントされません。ボタン操作による決定が行われるか、next.signal.aborted(ユーザーがEscを押した場合など)になるまでループして待機します。
  3. 多様な操作方法をサポートするボタン: Button({ label: "Proceed", hotkey: "1", onPress }) は、マウスクリック、Tabキーでのフォーカス後のEnterキー、または数字キー 1 や 2 の直接押下をサポートします。
  4. プロンプト入力欄上部へのフォールバック描画: ターミナル幅が十分な場合、パネルはトランスクリプト領域の右側に配置されます。$.ui.open が isPlaced: false を返した場合(例えば120列幅のターミナルでサイドパネルを配置する幅が足りない場合など)、同じレポートが入力欄の上部にフォールバックして描画されます。
120列のターミナルでBlast Radiusが入力欄の上に展開された様子
120列のターミナルでBlast Radiusがプロンプト入力欄の上にフォールバック表示された様子:git reset --hardによって破棄される未コミットの変更ファイル2件を表示し、番号付きのProceedとCancelの選択肢を提示している。

Blast Radiusは実行前に影響を示し、ユーザーが実行を許可するかどうかを判断できるようにします。これはコマンドのテキストマッチングに依存しているため、$(…) やコマンドのエイリアス(alias)、スクリプト内部で rm を呼び出すようなケースはこの検査をすり抜ける可能性があります。高リスクな操作をハードブロック(強制遮断)したい場合は、Claude Codeネイティブの権限ルール(Permission Rules)を使用する必要があります。

ケース 2:Replay Theater(単一ターンの変更内容をステップごとに振り返る)

1回の対話ターンにおいて、Replay TheaterはEditツールとWriteツールの各呼び出しについて、対象となったファイルおよび変更前後のテキストを記録します。ターンが完了するとプロンプト入力欄の上に案内が表示され、r を押す(または /replay と入力する)ことでパネルが開き、各編集の差分(diff)を1ステップずつ確認できます。パネルには数値によるステップバーと、Prev、Next、Closeボタンが用意されています。

このツールは変更内容のリプレイとレビューのみを目的としており、コードのロールバックは行いません。パネルを閉じてもファイルが変更されることはありません。

Replay Theaterターミナルデモ:1ターンの名前変更操作において、greet.js、main.js、README.mdの3ファイルにわたる5回の編集(greetをwelcomeに変更しgreetAllは維持)を記録。ターン完了後に案内が表示され、開いた後にステップごとにdiffをレビューし、閉じた後もファイルは変更後の状態を維持する様子。

このModは編集を一切ブロックせず、書き換えも行わないため、純粋な監視(Observe)フックに分類されます:

on("tool.call", async ($, e, next) => {
  if (EDIT_TOOLS.has(e.tool)) state.pending.push(...(await stepsFor($, e))); // 変更前/後のテキストからdiffを生成
  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;       // ターンごとにリプレイセットを生成
  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" }));

中核メカニズムと実装の詳細:

  • ペアとなるイベントでライフサイクルを管理:turn.start と turn.complete によって編集を1つのターン内にグルーピングし、!e.agentId をチェックすることでサブAgentによる内部ターンを除外します。
  • スラッシュコマンドの登録:session.start 内で $.command.register を呼び出してコマンドを登録し、command.run フックでその実行に応答します。
  • 書き込み前の旧コンテンツ読み取り:Write操作については、実際の書き込みを行う前に $.fs.read で古い内容を読み取ることで、変更前後の正確な差分(diff)を算出します。
  • レンダリングホストがレイアウトを決定:フルスクリーンターミナルではパネルが右側にドッキングされ、80列幅のターミナルではプロンプト入力欄の上にインラインで展開されますが、Mod内部からは同じコンポーネントツリーを返しています。
80列のターミナルでReplay Theaterがインライン展開された様子
80列のターミナルでReplay Theaterがプロンプト入力欄の上にインライン表示された様子:現在2/5ステップ目に位置し、greet.js内でgreetをwelcomeに変更した単一行のdiffを表示し、Prev、Next、Closeボタンを提供している。

四、 Mod作成時に意識すべき4つの習慣

公式の実装を踏まえ、開発時に参考にすべき4つの習慣を挙げます:

  1. システムが自動生成する型定義を参照する: Claude CodeがModをロードするたびに、現在のビルドバージョンの型定義が .claude-plugin/types/ に書き込まれます。イベントのフィールド、$ のAPIメソッド、UI要素でサポートされるプロパティなどは、すべてこのディレクトリから直接確認できます。
  2. コンポーネントのプロパティは e.props から取得する: ui.render イベントにおいて、e のトップレベルに存在するのは e.component、e.surface、e.requestId、e.viewport のみです。具体的なレイアウトやビジネスパラメータ(bodyColumns、hasSurvey など)はすべて e.props 内に格納されています。
  3. ホットリロードを考慮したデータストレージ設計: コードを保存してホットリロードがトリガーされるたびに、register と session.start は再実行されます。セッション内でリロードをまたいで保持したいデータはモジュールのトップレベル変数ではなく $.state に格納してください。
  4. 画面が表示されないときはデバッグログを確認する: コンポーネントが期待どおりにレンダリングされない場合は、claude --debug を付けて起動し出力を確認することで、フックが返したコンポーネントツリーがバリデーションで弾かれていないかを調査できます。

五、 応用のアイデアとセキュリティに関する考察

Token Weather、Blast Radius、Replay Theaterは、それぞれ「コンテキストをどれだけ使ったか」「高リスクなコマンドが何を変更しようとしているか」「直前のターンで具体的にどのコードが変更されたか」という3つの具体的な課題を解決しています。実際の開発において、公式はさらに探索可能な以下の方向性を提示しています:

  • コストと使用量の監視:$.session.usage() と $.ui.status を組み合わせ、ステータス行に使用金額やレート制限の状況を表示する。
  • プロンプト規約の注入:prompt.submit フックを介して、送信前にチームのコラボレーション規約をプロンプトに自動付加する。
  • 閲覧済みファイル一覧:セッション内で読み込まれたファイルをパネルに一覧表示し、コンテキストマップを直感的に可視化する。
  • 長時間ターンの完了通知:$.ui.toast を利用して、処理に時間がかかったターンが完了した際にトースト通知を表示する。
  • コマンド実行ガード:本番環境のコンテキスト(kubectl の本番クラスタ構成や terraform apply など)を対象とした専用の tool.call インターセプトルールを作成する。

チームでの活用と一元管理

個々の開発者の生産性向上だけでなく、Modはチームやエンタープライズレベルの開発標準化やセキュリティ統制にも役立ちます:

  • CI/CDステータスの表示:対話画面のサイドパネルにパイプラインのビルドステータスをリアルタイムで表示し、成功・失敗に応じて動的に更新することで、Webブラウザを頻繁に確認する手間を省く。
  • 本番設定操作前の再確認:本番環境の設定に触れる可能性のあるコマンドを実行する前に二次確認パネルを表示し、作業者が影響をしっかり把握してから実行できるようにする。
  • 監査用Modを最優先でロード:イベントチェーンにおいて最初にロードされたModは最も早くイベントを受け取り、最も遅く実行結果を受け取ります。チームは監査用Modを最優先でロードするよう設定することで、他のModの呼び出し状況を記録できます。

組織管理の観点では、Modはすべてプラグインとしてパッケージ化されて配布されるため、企業が既に導入しているプラグイン管理ルールがそのまま適用されます:

  • 管理コンソールとマネージド設定:TeamプランおよびEnterpriseプランでは、組織の管理者が管理コンソール(admin console)からプラグインマーケットプレイスを一括で許可またはブロックできます。Claude APIやサードパーティAPIを利用している環境では、管理者がユーザーのデバイスにマネージド設定(managed settings)を配信して一元管理できます。
  • 組み込みのセキュリティデフォルトMod(sec-default):TeamプランやEnterpriseプラン、およびマネージド設定が適用されたすべてのマシンでは、sec-default(security default)という組み込みModがデフォルトで最優先ロードされます。その主たる役割は、ユーザーが独自にインストールしたModを制限し、企業の事前設定された権限拒否ルール(permission deny rules)をModによって上書きされるのを防ぐなど、高リスクな権限昇格を防ぐことです。
  • 管理者がカスタムリストを設定する際の維持要件:管理者は自社開発のModを最優先でロードさせてカスタム統制を行うことも可能です。ただし、管理者が独自Modの優先ロードを指定する場合でも、その制約を維持するためにはリスト内に sec-default を明示的に残しておく必要があります。

セキュリティと信頼境界

Modのセキュリティを理解するには、実行環境とシステム権限という2つのレベルを区別して考える必要があります:

実行環境の面では、ModのランタイムにはブラウザDOMもNode.jsグローバルオブジェクトも存在せず、外部とのやり取りはすべて $ オブジェクトのAPI経由で行われます。しかし、この実行環境の分離はローカルマシンへのアクセスがサンドボックス化されていることを意味しません。Modはローカルデバイス上で実行され、Claude Codeと完全に同じシステム権限を持っています。

そのため、サードパーティ製Modのインストールにあたっては、通常のソフトウェアパッケージを導入する際と同じ原則に従う必要があります。コードは配布者によって記述されているため、インストール前にリポジトリのソースコードを確認し、信頼できる提供元のModのみをインストールしてください。ユーザーが能動的にインストールコマンドを実行しない限り、システムが外部のModを自動的に読み込むことはありません。

同時に、これはModが組織内で制御不能であることを意味するものではありません。エンタープライズやチーム環境では、管理者が管理コンソールを通じて信頼できないマーケットプレイスを無効化し、マネージド設定でポリシーを配信し、最前列に配置された sec-default や監査用Modを通じて動作の制限や証跡の記録を行うことができます。

Modは、1つの具体的なニーズから始めることができます。頻繁に確認するデータを手元に置く、実行前の影響を一覧表示する、あるいは自分の好みの方法でコード修正をステップ確認する——。イベントが「いつ動くべきか」を指示し、状態が「データを記憶」し、インターフェースが「目で見て操作する」ことを可能にします。

出典
Getting started with Claude Code modsAddy Osmani / Anthropic·原文を見る
↑ 本文ここまで
✓ この記事は全文無料です

メンバーは全文 Markdown のコピー・保存に加え、自分の AI に購読・質問させることができます。

原文を読む ↗
📮 新着解説をメールでいち早く

役に立ちましたか?メールを登録すると、新着解説を公開と同時にお届けします。解除はいつでも。