Claude Code の mods を作ってみる

Claude Code の mods を作ってみる

Claude Code の mods 機能を使用して、プロンプト添付画像をトークン効率化する実装方法を解説。余白削除と縮小処理で、50~69%のコンテキスト削減を実現します。
2026.11.13

Introduction

Claude Code には、その見た目や動作を TypeScript で
(ある程度)変更できる「mods」という拡張手法があります。

設定ファイルとスクリプトなどで記述する hooks と違い、
Claude Code の中で動作するコードとして、
プロンプトの送信やツールの呼び出しに割り込んだり、
スラッシュコマンドを足すなどの利用方法があります。

題材として
「プロンプトに添付した画像を、Claude に渡る前に小さくしてトークンを減らす mod」
をつくってみます。
※hooks からは添付画像が参照できず、mods でないと作れない例

題材の mod は、試した画像で、コンテキストの増加が 51〜69% 減りました。
(API の usage 使用)
ちなみに、自分の使い方だと、全体から見た効果は小さめでした。

コードはGitHub に置いてます。

Claude mods?

mods は、Claude Code の見た目と動きを変えられるプラグインです。
中身は JavaScript / TypeScript で書いたイベントハンドラで、
ツールの呼び出し・プロンプトの送信・画面の描画といったイベントが起きると
Claude Code がそれを呼び出します。
ハンドラはイベントの処理内容を変えることもできます。

※Claude Code v2.1.287 以降であれば標準で有効

最小の構成

mod は3つのファイルで作成できます。

my-mod/
├── .claude-plugin/plugin.json   … 名前・バージョン・説明
├── hooks/hooks.json             … 読み込むモジュール({ "modules": ["./register.ts"] })
└── hooks/register.ts            … フックを登録するモジュール

register.ts では、on(イベント, フック) でフックを登録します。
フックは ($, e, next) の3つを受け取ります。

  • $ — Claude Code の機能へアクセスするためのインターフェイス
  • e — イベントの中身
  • next(e) — 下の層(ほかの mod と Claude Code 本体)を呼ぶ関数。e を書き換えて渡せば、その内容で処理が進む
import type { Register } from 'claude-code'

export const register: Register = on => {
  // プロンプトが送信されるたびに呼ばれる
  on('prompt.submit', async ($, e, next) => {
    $.ui.log(e.text.length + ' 文字のプロンプト') // 会話に1行出す(モデルには送られない)
    return next(e)                             // そのまま通す
  })
}

mod から外部へアクセスするには $ を通じて行います。
claude plugin validate <mod> を実行すると、
その mod がどのイベントを拾い、$ の何を呼ぶかを読み込む前に一覧できます。

hooks(settings.json)との違い

Claude Code には以前から hooks がありますが、
こちらは「決まったタイミングで外部のコマンドを走らせる設定」で、mods とは別物です。

hooks は、設定ファイルに書いたコマンドが別のプロセスとして動きます。
mods は TypeScript のモジュールで、Claude Code の中で動きます。
そのため mods は、画面に表示を出したり(パネル・帯・通知・会話への1行など)、
スラッシュコマンドを足したりできます。
hooks でもメッセージの表示はできますが、パネルや帯の表示、スラッシュコマンドの追加はできません。

プロンプトの送信時に受け取れるものも違います。
hooks の UserPromptSubmit が受け取る入力には
prompt(テキスト)しかなく(Hooks reference)、添付した画像は見えません。
mods の prompt.submit には e.attachments があり、
$.prompt.media() で画像のバイト列まで読めます。

Environment

  • MacBook : macOS 26.5.2
  • Claude Code : 2.1.322(model : Claude Fable 5.2. 高解像度の扱いになるモデル)
  • ImageMagick 7.1.2-23(余白の切り落としに使用。無い場合 mac 標準の sips を使う)

※本記事の内容は2026年11月時点のものです

Image Resizing Mod

画像を最適化

Claude Code にスクショを貼って「このエラーを見て」と頼むことがあるかと思います。
このとき画像が使うトークンは、ファイルの容量ではなくピクセル数で決まります。

トークン数 = ⌈幅 / 28⌉ × ⌈高さ / 28⌉

PNG の圧縮率を高めたり JPEG の品質を下げたりしても、この数は変わりません。
減らせるのはピクセル数だけです。
Claude Code は添付画像を2000px まで縮小してから送りますが、
それでも Retina の画面全体のスクリーンショット(3014×1774px)は 2000×1177px、
約 3,100 トークンになります。

そこで、送信の直前にもう少しピクセル数を減らす、というのが題材の mod です。

方針

やりたいことは「添付された画像を、小さくした画像に差し替える」です。
mods の API によると、現時点ではそのままできないので、
今回は以下のように実装しました。

  1. prompt.submit で添付画像を取り出し、小さくして一時ファイルに保存する
  2. 「画像はこのファイルにあるので Read して」という指示を、プロンプトに添える(context)
  3. session.append で、会話に入る直前のメッセージから元の画像ブロックを外す

Claude から見ると、画像つきのプロンプトの代わりに「ファイルを読んで」という指示が届き、
Read ツールで小さい画像を読むことになります。(Read 方式)

使う mods の機能

この mod で使った機能の一覧です。以降の節で順に見ていきます。

機能 何に使ったか
prompt.submit フック 送信に割り込み、添付を読んで、Claude 向けの指示(context)を足す。送信の取り消しもできる
session.append フック 会話に入る直前のメッセージから、元の画像ブロックを外す
prompt.mention フック @ファイル で読むファイルを、小さくした写しにすり替える
$.prompt.media 添付画像のバイト列を読む
$.process.run 外部コマンド(ImageMagick など)を実行する
$.command.register と command.run フック スラッシュコマンド /imgdec を足す
$.store 設定をセッションをまたいで保存する
$.ui.log 会話に結果を1行出す
$.prompt.fill 入力欄に文字を書き足す

mod を作る

プロンプトの送信に割り込む(prompt.submit)

prompt.submit のフックは、プロンプトが送信されるたびに呼ばれます。
e.attachments に添付の一覧が入っていて、next(e) に渡す e を書き換えれば、
その内容で送信が進みます。
ここでは context に Claude 向けの指示を足しています。

//説明用に一部を省いています
on('prompt.submit', async ($, e, next) => {
  const images = (e.attachments ?? []).filter(a => a.type === 'image')

  // スラッシュコマンドの実行や、画像の無いプロンプトはそのまま通す
  if (e.command !== undefined || images.length === 0) {
    return next(e)
  }

  const settings = await readSettings($)
  if (!settings.isOn) {
    return next(e)
  }

  const dir = await makeDir($)        // 送信ごとの一時フォルダ
  const replaced: boolean[] = []      // 画像ごとに「外すか」
  const reduced: Reduced[] = []

  for (const [index, image] of images.entries()) {
    const one = image.id === undefined ? undefined : await reduce($, settings, dir, image.id, index)
    replaced.push(one !== undefined)
    if (one !== undefined) reduced.push(one)
  }

  if (reduced.length === 0) {
    return next(e)    // 小さくできた画像が無ければ、何もしない
  }

  const note =
    `${reduced.length} image(s) the user attached to this prompt were reduced ` +
    `and saved as files; the inline copies were removed. ` +
    `Read each file to view the image, and do not read the original image file:\n${reduced.map(one => one.path).join('\n')}`

  pending.push(replaced)    // session.append に渡す
  $.ui.log(reduced.map(one => one.summary).join(' / '))

  const entered = await next({ ...e, context: [...(e.context ?? []), note] })

  // ほかの mod などがプロンプトを止めたら、記録を取り消す(次の送信に残さない)
  if (entered.drop !== undefined) {
    const at = pending.indexOf(replaced)
    if (at !== -1) pending.splice(at, 1)
  }

  return entered
})

ポイントは以下。

  • 何もしないときは next(e) をそのまま返す。問題があれば元のプロンプトを通す
  • context に入れた文は Claude が読み、画面には出さない
  • next(e) の結果を見られる。ほかの mod が送信を止めた(drop)ときは、自分の記録を取り消す

会話に入るメッセージを書き換える(session.append)

prompt.submit では、添付画像そのものは外せません。
画像ブロックを外すのは、メッセージが会話に入る直前に呼ばれる
session.append のフック。
2つ目の引数 { door: 'prompt' } は絞り込みで、
プロンプトから入るメッセージだけを対象にしています。

on('session.append', { door: 'prompt' }, ($, e, next) => {
  const replaced = pending[0]

  if (replaced === undefined || !e.message.content.some(b => b.type === 'image')) {
    return next(e)
  }

  pending.shift()

  let index = 0
  const content = e.message.content.filter(
    block => block.type !== 'image' || replaced[index++] !== true,
  )

  return next({ ...e, message: { ...e.message, content } })
})

2つのフックは別々に呼ばれるので、
どの画像を外すかはモジュールの変数(pending)で受け渡しています。
複数枚を添付したときは、小さくした画像だけを外し、そのまま通す画像は残します。

外部コマンドを呼ぶ($.process.run)

mod の実行環境には Node が無いので、sharp のような画像ライブラリは使えません。
画像の処理は、$.process.run で外部のコマンドを呼んで行います。

// 画像を書き出す(base64 の文字列を標準入力から渡す)
await $.process.run(['sh', '-c', 'base64 --decode > "$1"', 'sh', path], { stdin: base64 })

// 大きさと、余白を切り落としたときの枠(%@)を調べる。ここでは何も書き出さない
await $.process.run(['magick', path, '-fuzz', '2%', '-format', '%w %h %@', 'info:'])
//   → "1672 941 1546x737+63+102"

// ① 余白の切り落としと ② 縮小を、1回で書き出す
await $.process.run(['magick', path, '-crop', '1546x737+63+102', '+repage', '-resize', '1024x1024>', path])

一律に縮小すると文字も一緒に小さくなりますが、余白を削るだけなら文字の大きさは変わりません。
なので、余白の切り落としを先に行い、それでも大きいときだけ縮小しています。
ImageMagick が無い macOS では標準の sips で縮小だけ行い、
どのツールも無ければ画像をそのまま通します。

小さくした画像は、mktemp -d で作ったフォルダに置きます。
最初は /tmp/claude-imgdec/ という固定のフォルダでしたが、
/tmp はマシン上の全ユーザーがREAD可能なので変えました。

mktemp -d のフォルダは自分だけが読める権限(700)で作られます。
mod は入れた人の権限でそのまま動くので気をつけましょう。

スラッシュコマンドと設定の保存($.command・$.store)

細かい文字を元の解像度で読ませたいときに止められるよう、スラッシュコマンドを足しました。

コマンド 動き
/imgdec 今の設定と、直近の結果を表示
/imgdec off / on 止める / 再開する
/imgdec max 1280 長辺の上限を変える(256〜2000px。既定は 1024)
/imgdec mode resend / read 渡し方を変える(あとの節で説明。既定は read)

コマンドは session.start で登録し、command.run のフックで答えます。
設定は $.store に保存するので、セッションをまたいで残ります。

on('session.start', async ($, e, next) => {
  await $.command.register({
    name: 'imgdec',
    description: 'Reduce attached images before Claude reads them',
    argumentHint: 'on|off|max <px>|mode read|resend',
    immediate: true,   // Claude の作業中でもすぐ実行できる
  })
  return next(e)
})

on('command.run', { command: 'imgdec' }, async ($, e) => runImgdec($, e.args))

command.run のフックが返す { text } の text が、コマンドの出力になります。

imgdec: ON
- Mode: read
- Max edge: 1024px
- Trim margins: on
- Image tool: ImageMagick
- Saved to: /var/folders/…/T/claude-imgdec.*/
- Last result:
  - 2000×1177 → 1024×603 (resize, ~3,096 → 814 tokens) /var/folders/…/T/claude-imgdec.UeADqs/0.jpg
Usage: /imgdec [on|off|max <px>|mode read|resend]

@ の読み込みに割り込む(prompt.mention)と入力欄の操作($.prompt.fill)

Read 方式は、Claude が Read を呼ぶぶん API のリクエストが1回増えます。
会話が長いほど、この1回のコストが高くなります。
なので、Read を挟まない方法も試しました。
@ファイル で指定した画像は Claude Code が Read なしで直接添付する、という動作を利用します。

※ @ が画像に展開されるのは、人が自分で送信したプロンプトだけ
※ mod が本文に @パス を書き足したり、mod から送信し直したりしても展開されない

① @ で指定された画像をすり替える
prompt.mention のフックは、読むファイルのパスを書き換えられます。

on('prompt.mention', async ($, e, next) => {
  const one = await copyAndShrink($, e.path)   // 写しを作って小さくする
  if (one === undefined) return next(e)
  const attached = await next({ ...e, path: one.path })   // 読むファイルをすり替える
  // 「添付したのは縮小した写し。元のファイルは読まないで」という Claude 向けの注記を足す
  return { ...attached, context: [...(attached.context ?? []), MENTIONED_NOTE] }
})

② 再送信モード(/imgdec mode resend)
ドラッグ&ドロップや貼り付けの画像は、送信をいったん取り消して、
入力欄に @縮小済みファイル を書き足します。
prompt.submit のフックが { drop } を返すと送信は取り消され、
入力欄の内容は元に戻ります。
もう一度 Enter を押すと、今度は人の送信なので @ が展開されます。

const filled = await $.prompt.fill({ text: mentionsOf(paths), mode: 'append' })
if (filled.isFilled) {   // 書き足せたときだけ、送信を取り消す
  return { drop: 'Image reduced. Press Enter again to send it.' }
}

入力欄には元の画像も残っているので、2回目の送信では Read 方式と同じく、session.append で元の画像ブロックを外します。
外さないと、元の画像と縮小した写しの両方が届きます。

会話に結果を出す($.ui.log)

小さくしたときは、$.ui.log で送信した発言のすぐ上に結果を1行出しています。
この行はモデルには送られません。

imgdec: 2000×1177 → 1024×603 (resize, ~3,096 → 814 tokens)

左側の 2000×1177 は、Claude Code 本体が縮小した後のサイズです。
※ mods はほかにも、パネルや入力欄の上の帯、ステータスラインへの表示が可能

配布とインストール

mod はプラグインとして配布できます。
配る側は、GitHub のリポジトリに「マーケットプレイス」の定義を1つ置くだけ。

claude-mod-imgdec/                  … リポジトリ
├── .claude-plugin/marketplace.json … マーケットプレイスの定義
└── imgdec/                         … mod 本体(前述の3ファイル)
{
  "name": "claude-mod-imgdec",
  "owner": { "name": "nakamura-shuta" },
  "plugins": [{ "name": "imgdec", "source": "./imgdec" }]
}

使う側は、マーケットプレイスを登録してから、
<プラグイン名>@<マーケットプレイス名> でインストール。

# 余白の切り落としに使う(macOS は無くても縮小だけは動く)
brew install imagemagick  # Linux は sudo apt install imagemagick

claude plugin marketplace add nakamura-shuta/claude-mod-imgdec
claude plugin install imgdec@claude-mod-imgdec

起動中のセッションには /reload-plugins で反映されます。
それ以外は、次に Claude Code を起動したときに読み込まれます。

なお、pluginを更新したいときは、plugin.json の version を上げて push します。
使う側は以下でバージョンアップ可能。

claude plugin marketplace update claude-mod-imgdec
claude plugin update imgdec@claude-mod-imgdec

アンインストールは claude plugin uninstall imgdec@claude-mod-imgdec です。

Result

手元で試した結果です。トークン数は、画像の寸法を前述の式に当てはめた概算です。

画像 mod なし(本体の縮小後) mod あり 処理 削減
画面スクショ A(3014×1774) 2000×1177・約 3,096 1024×603・約 814 縮小 約 74%
画面スクショ B(3018×1742)、上限 1280px 1999×1154・約 3,024 1280×739・約 1,242 縮小 約 59%
テスト画像(2400×1600) 2000×1333・約 3,456 1024×682・約 925 縮小 約 73%
余白の多い図(1600×1000) 1600×1000・約 2,088 801×421・約 464 余白カット 約 78%
文字が1行だけの画像(800×600) 800×600・約 638 578×36・約 42 余白カット 約 93%

読みやすさは、上限によって差が出ました。
範囲を絞ったスクリーンショットなら 1024px で足りますが、
画面全体を撮ると、一覧の小さな説明文などはつぶれ気味になります。
細かい文字まで読ませたいなら 1280px 以上がよさそうです。

usage で測る

claude -p で条件ごとに新しいセッションを起動し、API の usage で測りました。
表の数値は、画像なしの場合を基準にした、最後のリクエストの入力トークン(通常の入力+キャッシュ書き込み+キャッシュ読み出し)の増加分です。
※モデルは Fable 5.2、上限は 1024px

渡し方 リクエスト 図(1672×941、余白あり) 単純な絵(2200×1400)
元のまま(mod なし) 1回 2,164 3,476
Read 方式 2回 1,059(−51%) 1,320(−62%)
@ のすり替え 1回 849(−61%) 1,071(−69%)
縮小済みの画像を直接 @ で指定 1回 828(−62%) 1,051(−70%)

最後の行は再送信モードの参考値です(実際の再送信では、短い注記のぶんが少し加わります)。

どの渡し方でもコンテキストの増加が半分以下になりました。
Read 方式だけリクエストが1回増えるので、長い会話では @ か再送信のほうが有利。

全体から見ると効果は小さい

画像1枚ぶんは半分以下になりましたが、
自分の使い方では、Claude Code 全体のトークン削減という観点でみると効果はほとんどありませんでした。

(私の場合)1週間ぶんのセッション記録のトークン数をざっくり集計したところ、
ほとんどは長い会話やサブエージェントでのキャッシュの読み直しで、
画像を毎日 10 枚くらい貼ると見積もっても、減らせるのはトークン数の合計の 1% にも届かない程度です。

上限を気にするなら、こういった手法のほうが はるかに効果的です。
スクリーンショットを何百、何千枚も貼るタスク(画像が中心の処理)なら多少効果あるかもしれません。

Tips

mod を作っていて気づいた点など。

  1. $.ui.log には mod の名前が自動で付く
    $.ui.log('imgdec: …') と書いたら、画面には imgdec: imgdec: … と出ました。
    名前は Claude Code が付けるので、自分では書かなくてよいです。

  2. @ で添付できる画像にはファイルサイズの上限がある
    @ で指定した画像は、ファイルが 256KiB を超えると、
    エラーも出ずに何も添付されませんでした(Claude Code 2.1.322 で確かめた範囲)。
    そのため @ と再送信では、写しを 200KB 以下の JPEG にしています。

  3. 外部コマンドは OS で動きが違う
    sips -Z 1024 は「長辺を 1024px にする」動きで、小さい画像は引き伸ばされます。
    base64 のオプションも macOS と Linux で違いました(両方で通るのは base64 --decode)。
    $.process.run で呼ぶコマンドは、対応する OS ごとに確かめる必要があります。

Other Tools

今回紹介したようなツールはすでにいろいろあります。

名前 仕組み 違い
paste-preview mod。貼り付けた画像に注釈を付けて、編集後のファイルを Read させる 仕組みは今回とほぼ同じだが、目的は注釈で、縮小はしない
token-goat hooks と MCP。Read する画像や、ツールが返す画像を縮小する プロンプトに添付した画像の扱いは書かれていない
vision-squeezer MCP と hooks。Read する画像をトークン予算に収める 貼り付け・ドラッグした画像は縮小できないと明記されている

Summary

mods の紹介として、添付した画像を小さくする mod を作ってみました。
mods 自体は簡単に試せますし、
hooksだけではできないことも実現できたりするので、
気になる方は触ってみてください。

References


Claudeならクラスメソッドにお任せください

クラスメソッドは、Anthropic社とリセラー契約を締結しています。各種製品ガイドから、業種別の活用法、フェーズごとのお悩み解決などサービス支援ページにまとめております。まずはご覧いただき、お気軽にご相談ください。

サービス詳細を見る

この記事をシェアする

AI白書

関連記事