· Tutorials

Claude CodeのMODの始め方

Getting started with Claude Code mods

この記事は 原文 を DeepL で機械翻訳したものである。正確な内容は原文を参照のこと。

Modとは、Claude Codeのセッション内で実行される小さなJavaScriptまたはTypeScriptのファイルのことです。Modは、ターミナルやデスクトップアプリ上で、現在の状況を監視したり、Claude Codeの動作を変更したり、独自のUIを描画したりすることができます。Modを試すためにAPIを学ぶ必要はありません。 claudeを実行し、希望するmodを記述してください。プロンプトが表示されたらホットリロードを許可すると、ターン終了時にmodが反映されます。

Claude Code すでに、設定、権限ルール、スラッシュコマンド、スキル、ステータス行など、動作の多くの部分を変更することができます。Modsはさらに一歩進んでおり、Claude Codeの動作を書き換えたり置き換えたりできるほか、カスタムUIを描画することも可能です。内部的には、Modsはプラグイン内に組み込まれたフックであり、各Modsはセッション内で発生するすべてのイベントをリアルタイムで検知します。

つまり、MODsを使えば、Claude Codeを自分の作業スタイルに合わせてカスタマイズできるのです。常に確認したい表示を追加したり、不安を感じるコマンドの前にガードを設けたり、変更内容を自分の好みの方法で確認できるレビュービューを作成したりすることができます。

このガイドでは、空のフォルダから「Token Weather」というMODを1つ作成します。これは、プロンプトの上に表示されるコンテキストウィンドウのリアルタイム天気予報です。コードは約80行です。その後、「Blast Radius」と「Replay Theater」という2つの大規模なMODを紹介し、APIで他にどのようなことができるかを解説します。

図A ターミナルセッション内で、「Token Weather」、「Blast Radius」、「Replay Theater」が次々と表示される様子
図B コンテキストウィンドウが埋まっていくにつれて、Token Weatherの帯が変化する様子:18%で「晴れ」、67%で「にわか雨」、81%で「嵐」

Claude Code 2.1.287 以降。MOD はデフォルトで有効になっているため、有効にする必要はありません。API はリリースごとに変更される場合があります。Claude Code が MOD を読み込むたびに、そのビルド用の型宣言が MOD の .claude-plugin/types/ フォルダに書き込み、それらがそのバージョンの基準となります。

MODの仕組み

modとは、その動作がJavaScriptまたはTypeScriptモジュール内に実装されているClaude Codeプラグインのことです:

  • このフォルダは通常のプラグインであり、 .claude-plugin/plugin.json マニフェストが含まれています。
  • hooks/hooks.json 「」の下にある1つのモジュールを指定します modules.
  • このモジュールは register(on, options)をエクスポートします。その内部では、 on(event, matcher?, hook) フックを追加します。

どのフックも同じ形をしています:

JavaScript
on("tool.call", { tool: "Bash" }, async ($, e, next) => {
  // $    the mods API: ui, session, state, store, fs, process, clock, http, tool, command, model, ...
  // e    this event's input, as plain data
  // next passes e to the other plugins and then to Claude Code's own behavior
  return next(e);
});

フックは、ミドルウェアのように連鎖を形成します。あなたのフックが実行されると、 next(e) イベントを次のプラグインに渡すと、最下層のClaude Codeが本来行うべき処理を実行します。フックは次の3つのうちのいずれかを実行できます:

フックがつながって鎖になる
移動方法例
観察するconst r = await next(e); /* 確認 */ return rファイルの編集内容をすべて記録してください。各ターン終了後に測定を行ってください。
書き換えreturn next({ ...e, command: safer })チェーンの残りの部分が認識する内容を変更します。
回答return { deny: "…" } 電話をかけずに nextツール呼び出しを拒否する。コマンドまたはツールを自分で実行する。

これらのイベントには、ツールの呼び出し、送信されたプロンプト、ターンの開始と終了、セッションの開始と終了、スラッシュコマンド、そして ui.render:描画されるインターフェースのあらゆる要素を網羅しています。このモジュールは独自のサンドボックス内で動作し、DOMもNodeも存在しないため、その外部のすべての処理は $.

設定フックとの違い。 設定フックは、イベントごとにシェルコマンドを実行し、JSONデータをstdinおよびstdout経由で渡します。一方、modは一度読み込まれるとセッション中に保持されます。modは状態を保持したり、イベントの発生に合わせて更新されるUIを描画したり、Claude Codeへのコールバックを行ったりすることができます。具体的には、ペインを開いたり、プロセスを実行したり、スラッシュコマンドを登録したり、モデルが呼び出せるツールを登録したりすることが可能です。

Claude Code それ自体でそれらを使用しています。Claude Code独自の機能の一部はMODとして構築されており、AGENTS.mdのサポートや、 /diff 会話の横にあるペインなどが挙げられます。これらのソースコード(テストを含む)は、公開リポジトリ anthropics/claude-code の mods/に公開されているため、チームがどのようにそれらを構築しているかを確認できます。

初めてのMODを作ろう:TOKEN WEATHER

Token Weatherは、各ターン終了後にコンテキストウィンドウの残量を確認し、プロンプトの上に1行のラインを描画します。そのラインには、天気アイコン、残量(パーセンテージ)、ウィンドウから使用されたトークンの数、直近のターンの推移を示す小さなグラフ、そして前回のターンで追加された量が表示されます。

中古予報
25%未満☀ 晴れ
25~49%☁ 曇り
50~74%☂ にわか雨
75~89%☇ 嵐
90%以上↯ まもなくコンパクト版が登場

実際のセッションの様子はこちらです。ターンごとにファイルが読み込まれ、帯が「☀ 晴れ」から「☂ にわか雨」、そして「☇ 嵐」へと変化していきます:

図C:3ターンにわたるフルターミナルセッションにおけるトークンウェザー:200kウィンドウの18%、次に67%、そして81%

手っ取り早い方法:Claudeにビルドさせる

6つの手順はスキップしても構いません。Claude CodeはMODの作成方法を知っているので、希望するMODの内容を説明すれば、あとはに任せて作業を行わせることができます。以下のプロンプトを貼り付けてセッションを開始してください。 claude でセッションを開始し、以下のプロンプトを貼り付けてください:

Text
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 そのセッションでホットリロードを有効にするかどうかを一度だけ尋ねます。許可すると、Claudeのターンが終了した際に、プロンプトの上にバンドが表示されます。 それ以降は、変更のたびにその場で再読み込みが行われるため、「Stormを70%から開始するように」「最後にドルコストを追加するように」といった微調整を繰り返し依頼し、バンドの変化を確認することができます。 このMODは当該セッションでのみ読み込まれ、終了後にフォルダは削除されます。保存したい場合は、フォルダをコピーして、通常のプラグインと同様にインストールしてください(手順6)。

このプロンプトは、単に「何を見たいか」を記述しているだけであることに注意してください。プロンプトを作成するのに、APIの知識は必要ありません。Claude Codeに組み込まれているMOD作成ガイドには、その方法(リロード後も状態が保持されるように状態をどこに保存するか、 claude plugin validateでプラグインを検証する方法、どのイベントにフックすべきかなどについて解説しています。「表示すべき内容」の行を変更すれば、それは私たちのモッドではなく、あなた自身のモッドになります。

まずはその構成を確認したい方や、Claudeの記事を読みたい方は、このまま読み進めてください。

手順 1:フォルダを作成する

お使いのClaude Codeが十分に新しいバージョンであることを確認してください:

Shell
claude --version   # 2.1.287 or later

次のレイアウトを作成してください:

Text
token-weather/
├── .claude-plugin/
│   ├── plugin.json
│   └── types/            (written by Claude Code when it loads the mod)
├── hooks/
│   ├── hooks.json
│   └── token-weather.mjs
├── types/
│   └── index.d.ts        (added in step 3)
└── tests/
    └── token-weather.test.ts   (added in step 5)

.claude-plugin/plugin.json これは標準的なプラグインマニフェストです:

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 そのモジュールを指しています。モジュールには、これがちょうど1つだけあります:

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

ステップ2:何かを描いてみましょう

プロンプトのすぐ上にある帯は、 AbovePromptです。Claude Code自体はそこに何も描画しないため、最初のターゲットとして適しています。その ui.render イベントをフックし、要素のツリーを返します:

JavaScript
// 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" })],
    });
  });
}

これらの要素はグローバル変数ではありません。 $.ui.resolve(e) 描画中のサーフェスのコンストラクタを返します。これは、Claude Codeが描画を行う各サーフェスが、それぞれわずかに異なるセットをサポートしているためです。JSXも同様に機能し、 h をファクトリとして使用すれば、JSXも動作します。

プラグインを読み込んだ状態でセッションを開始します:

Shell
claude --plugin-dir ./token-weather

プロンプトの上に「☀ 晴れ」と表示されます。セッションは閉じてはいけません。フォルダは監視されているため、保存するたびにモジュールがその場で再読み込みされ、再起動は不要です。この素早いフィードバックループこそが、MOD作成の楽しさの大部分を占めています。

ヒント:形状が決まったら、ショートカットと同じ要領で、Claude に次のモッドを記述してください。これにより、プラグインがフォルダに書き込まれ、同じセッション内でホットリロードされます。

ステップ3:実数を読み取り、それを保持する $.state

$.session.usage() ステータス行と同じ数値を返します。 context.tokens は、前回の応答が行われた入力であり、 context.window はモデルのウィンドウであり、 context.percent は前者の逆数です。この呼び出しは無料です。トークンカウントのリクエストが送信されるのは、 breakdown.

セッションの開始時と、各ターンの終了後に測定を行ってください:

JavaScript
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($); // main-loop turns only, not subagents
  }
  return result;
});

どちらのフックも、 next(e) まず実行し、その後オブザーブします。どちらの場合も、結果に変化はありません。

測定値をどこに保存するか。モジュールレベルの let readings = [] が当然の選択肢のように思えますが、ホットリロードは新規読み込みとなるため、 register 再実行され、 session.start 再び実行され、モジュール変数はリセットされます。履歴は $.state に保存してください。これなら、セッション全体を通じてホストに名前付き値が保持され、リロード後も値が維持されます。

JavaScript
// Held by the host, so the history survives a hot reload of this file.
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));
}

プラグインの型契約には状態値が宣言されています。これは、マニフェストが指す小さな .d.ts ファイルであり、マニフェストから参照されます。以下を追加してください。 types/index.d.ts:

TypeScript
export type TokenWeatherReading = { tokens: number; window: number; percent: number };

declare module "claude-code" {
  interface PluginState {
    "token-weather": { readings: TokenWeatherReading[] };
  }
}

次に、 "types": "./types/index.d.ts" を plugin.jsonに追加します。この手順を省略すると、 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:予測を描く

以下にモジュール全体を示します:

JavaScript
// Token Weather: a live forecast of the context window, above the prompt.

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" },
];

// Held by the host, so the history survives a hot reload of this file.
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($); // main-loop turns only, not subagents
    }
    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);
}

以下の3つのポイントは、自分のMODに取り入れる価値があります:

  • コンポーネントのプロップは `e.props` にあります。 hasSurvey これは、surveyがbandを要求していることを示しているため、hookは next(e). bodyColumns はバンドの実際の幅であり、トランスクリプトの横にペインがドッキングされている間は、ターミナルよりも狭くなります。ツリーのサイズをこれに合わせてください。ただし e.component, e.surface, e.requestId および e.viewport のみが e.
  • 描くものが何もないときはパスしてください。 next(e) を選択すると、バンドがClaude Codeや他のMODに戻ります。
  • 絵文字ではなく、シングル幅の記号を使用してください。☀ ☁ ☂ ☇ ↯ は、どのターミナルフォントでも揃って表示されます。

ファイルを保存すると、実行中のセッションがそれを反映します。大容量のファイルを読み込むターンを数回繰り返すと、このセクションの冒頭の録音にあるように、バンドの状態が「Clear」から「Showers」、そして「Storm」へと変化します。

ステップ 5:検証とテスト

claude plugin validate Claude Code と同様にマニフェストとモジュールのソースを読み込み、そのモジュールがどのようなフックや呼び出しを行っているかを報告します:

Text
$ 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

claude plugin test プラグインの *.test.ts ファイルを実際のClaude Codeランタイムに対して実行します。テストが登録するフックは on は、チェーン内のモジュール処理の後に実行され、Claude Codeが返すはずの値をスタブ化するため、 $.session.usage() を正確に制御できます:

TypeScript
// 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) => {
    // Hooks registered here run after the mod and stub what Claude Code would answer.
    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);
    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: /67% of context/ })).toBeDefined();
    expect(await ui.find({ type: "Text", text: /▲ \+98\.3k last turn/ })).toBeDefined();
    await ui.unmount();
  });
});
Text
$ claude plugin test ./token-weather
(pass) token-weather > the band follows the context window
 1 pass
 0 fail

このテストでは、ステップ3での再描画の挙動も確認します。バンドは、 turn.complete 、モジュールが再描画を要求することなく更新されます。

ステップ6:共有する

MODはプラグインの一種なので、配布方法も同様です。マーケットプレイスに掲載しますが、その方法は、単にフォルダに .claude-plugin/marketplace.json:

JSON
{
  "name": "my-mods",
  "owner": { "name": "You" },
  "plugins": [{ "name": "token-weather", "source": "./token-weather" }]
}
Shell
claude plugin marketplace add ./my-mods
claude plugin install token-weather@my-mods --scope user

MODの共有

ModはClaude Codeのプラグインであるため、他のプラグインと同じ方法で共有でき、新たに学ぶべきことは何もありません。ModをGitHubのリポジトリにマーケットプレイスファイルとともに配置すれば、そのリポジトリがあなたのマーケットプレイスになります。誰でもそこからインストールでき、通常のプッシュ操作で更新を行うことができます。

Claude Code でのインストールには、3つのコマンドを実行します:

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

このMODはリロードすると起動します。表示されない場合は、Claude Codeを再起動してください。

MODとは、お使いのマシン上のClaude Code内で実行されるコードのことで、Claude Codeと同じアクセス権限を持ち、Anthropicではなくその発行元によって作成されています。したがって、MODのインストールはパッケージをインストールするのと同じ要領で行ってください。まずリポジトリの内容を確認し、信頼できる提供元からのもののみをインストールしてください。コマンドを実行するまでは、何もインストールされません。

Claudeディレクトリでは、MODを含むプラグインを受け付けています。claude.ai/directory/manage からご自身のプラグインを投稿すれば、あなたがリンクを貼らなくても、他のユーザーがそれを見つけられるようになります。

さらに2つのMOD

「Token Weather」は、単に監視して描画するだけです。次の2つのMODでは、イベントの処理やペインの表示、入力の受け付けも行います。

爆発範囲:危険なコマンドを実行する前に、それがどのような変化をもたらすかを確認する

Claudeが rm -rf, git reset --hard, git clean、フォースプッシュ、またはデータベースのマイグレーションが行われると、Blast Radiusはその呼び出しを一時停止します。Blast Radiusは、そのコマンドがどのリソースに影響を与えるかを解析し、「続行」と「キャンセル」の選択肢が表示されたパネルを開きます。 2 を選択すると、Claudeには理由付きの拒否通知が表示されます。 1 を押すと、コマンドは記述どおりに実行されます。

図D「Blast Radius」は「rm -rf build」を実行し、削除される9つのファイル(1.1 MB)を一覧表示します。「Cancel」を選択すると実行は中止されますが、2回目の試行で「Proceed」を選択すると実行されます。

このスクリプトでは、3つのフックを使用しています: tool.call Bash用、および ui.render および Pane および AbovePrompt。その中核となるのは、上の表にある「answer」という動きです:

JavaScript
on("tool.call", { tool: "Bash" }, async ($, e, next) => {
  const risk = classify(String(e.command ?? ""));
  if (risk === null) return next(e);                 // everything else runs as normal

  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";         // too narrow for a pane: draw above the prompt

  while (held.decision === null && !next.signal.aborted) {
    await $.process.run(["sleep", "0.25"]);          // time inside $ calls doesn't count against the hook's time limit
  }
  if (held.decision === "proceed") return next(e);   // let it run
  return { deny: `Blast Radius held this command: the user pressed Cancel. It would have: ${report.summary}.` };
});

そこから学べることは:

  • $.process.run ドライラン用です。このレポートは、ツール自体のコマンドから得られたものです: git status --porcelain, git clean -n, git log HEAD..origin/main, showmigrations。引数はargv配列として渡されるため、パス内のどのファイルもシェルコードとして実行されることはありません。
  • 通話を保留中。フックはディスパッチごとに10秒間の処理時間を割り当てられますが、 $ 通話内での待機時間はカウントされません。ループは、ボタンが押されるまで短い sleep 処理を待機し、ボタンが onPress 決定が行われるまで待機し、 next.signal (Escキーが押された場合)処理が中断されると、ループは終了します。
  • ホットキー付きのボタン。 Button({ label: "Proceed", hotkey: "1", onPress }) クリック、TabキーとEnterキーの組み合わせ、または数字を入力することで動作します。
  • バンドに縮小表示されます。画面の幅が十分にある場合、ターミナルはトランスクリプトの横にペインを表示します。 $.ui.open 回答 isPlaced: falseすると、プロンプトの上に同じレポートが表示されます:
図 E 120 列の位置で、Blast Radius はプロンプトの上の帯に「git reset --hard」に関するレポートを表示します。
図 E 120 列の位置で、Blast Radius はプロンプトの上の帯に「git reset --hard」に関するレポートを表示します。

これは安全策であり、権限管理システムではありません。コマンドのテキストを読み取るため、 $(…)、これを rm は通過できます。完全にブロックしたい場合は、権限ルールを使用してください。

リプレイシアター:前回のターンでの編集内容を順を追って確認する

ターンが進行している間、「リプレイシアター」はすべての「Edit」および「Write」の呼び出しを記録します。具体的には、ファイル名に加え、その前後のテキストも記録されます。ターンが終了すると、プロンプトの上にヒントが表示されます。 r キーを押すか(または /replay)を押すと、編集内容を1つの差分ずつ順に確認できるペインが表示され、番号付きのステップ一覧と「前へ」「次へ」「閉じる」ボタンが表示されます。

図 F リプレイシアター:3つのファイルにわたる5回の編集と名前変更を行った後のヒント、続いてペイン内の手順1から5

編集をブロックしたり変更したりすることは決してありません。ただ、次のように観察するだけです:

JavaScript
on("tool.call", async ($, e, next) => {
  if (EDIT_TOOLS.has(e.tool)) state.pending.push(...(await stepsFor($, e)));  // old/new text → diff
  return next(e);                                                              // the edit runs untouched
});

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;       // one replay per turn
  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ターンにつき1つのリプレイにまとめ、 e.agentId サブエージェントのターンはグループ化の対象外とする。
  • スラッシュコマンドの登録。 $.command.register in session.startで、その後、そこで回答してください。 command.run.
  • ファイルの読み込み。書き込みの場合、 $.fs.read 書き込みが反映される直前の古い内容を取得するため、差分は実際のものです。
  • 配置はワークスペースの役割です。フルスクリーン表示では、ペインは右側にドッキングされます。80列の場合、プロンプトの上部にインラインで表示されます。どちらの場合でも、モジュールは同じツリーを描画します。
80列分のFIG G リプレイシアターを、プロンプトの上部にインラインで描画
80列分のFIG G リプレイシアターを、プロンプトの上部にインラインで描画

身につけておく価値のある4つの習慣

  • Claude Codeが自動生成してくれる型情報を活用しましょう。MODが読み込まれるたびに、Claude Codeがビルド用の宣言をMODの .claude-plugin/types/ フォルダにビルド用の宣言を書き出します。これにより、エディタや tsc -p 追加の手順なしに動作します。これらは、すべてのイベント、各メソッド $ 各要素のプロパティに関するリファレンスとなります。
  • e.props からプロパティを読み込みます。 hasSurvey, bodyColumns そして、残りのものはそこにあり、 e そのサイト自体にはありません。
  • ホットリロードを想定してください。セーブが行われるたびに register 実行され session.start 実行されるため、データは $.stateに保存し、モジュール変数には保存しないでください。
  • 図面が表示されない場合は、ログを確認してください。 claude --debug を実行し、フックが検証に失敗したツリーを返したという記述がないか確認してください。

何を改造しますか?

ここで紹介する3つのMODは、それぞれ「コンテキストはどの程度埋まっているか?」「このコマンドは何を削除しようとしているのか?」「Claudeは何を変更したのか?」という1つの疑問から生まれました。皆さんの疑問はそれぞれ異なるでしょうが、それこそが重要な点です。まずは以下のアイデアを参考にしてみてください:

  • 以下のコストまたはレート制限メーター $.session.usage()のコストまたはレート制限メーターを、ステータス行として $.ui.status
  • A prompt.submit すべてのプロンプトにチームの規約を追加するフック
  • このセッションでClaudeが読み込んだファイルを一覧表示するペイン。これは、がこれまでに読み込んだファイルのリアルタイムなマップとして機能します。
  • 長いターンが終了した際に、 $.ui.toast 長いターンが終了した際にトーストを表示する
  • A tool.call スタックに合わせて設定されたガード(本番環境の kubectl コンテキストなど)や terraform apply

作ったものをシェアしよう

毎日使っているMODを作りましたか?その動作中のGIFやスクリーンショットを添えて、XやLinkedInに投稿しましょう。そうすれば、他の開発者もその可能性を実感できます。また、そのプラグインをマーケットプレイスに掲載し(MODの共有)、リンクを貼っておけば、気に入った人は3つのコマンドでインストールできるようになります。