Claude Mods の全体像 .

Claude Code の Mods(function hooks)で何ができて、どう書き、どう始めるのかを 1 ページにまとめました。ハッカソンの補足資料として使えます。

対応バージョン Claude Code v2.1.299 最終更新 チートシート →

Mods とは.

Claude Code の振る舞いと見た目を、TypeScript / JavaScript の関数で書き換えるプラグインです。

土台は function hook という仕組みです。イベントごとに async 関数を登録し、Express や Koa のミドルウェアのように重ねて合成します。mod は「hooks/hooks.json がこのモジュールを 1 つ指しているプラグイン」で、配布もインストールも普通のプラグインと同じです。

止める・書き換える

ツール実行、プロンプト送信、設定変更などを実行前に受け取り、拒否したり中身を書き換えたりできます。

tool.call · prompt.submit · config.set

描く

サイドパネル、プロンプト欄の上の帯、ツール結果の表示などを自分の UI に差し替えられます。

ui.render · $.ui.open · $.ui.toast

足す

スラッシュコマンド、モデルが呼べるツール、サブエージェントの型を追加できます。

$.command · $.tool · $.agent

従来の hooks との違い

従来の hooks(settings) Mods(function hooks)
動く場所 外部のシェルプロセス Claude Code のプロセス内の関数
やりとり JSON / テキストと allow・deny 値をそのまま書き換え、UI も返せる
タイミング 決まった時点で 1 回 next() の前後どちらでも介入できる
UI・状態 持てない パネル描画、状態保持、タイマー
複数の組み合わせ 順番の制御が難しい 登録順のチェーンで自然に合成
  • ・ターミナルは v2.1.287 以降、デスクトップアプリは v2.1.286 以降で既定でオン(フラグ不要)
  • ・従来の settings の hooks はそのまま動きます。mod からは classic.PreToolUse などとしてフックできます

基本形.

モジュールは register(on, options) を export し、その中で on(イベント名, マッチャー?, フック) を呼びます。フックはすべて ($, e, next) の形です。

export function register(on) {
  on("tool.call", { tool: "Bash" }, async ($, e, next) => {
    if (e.command.includes("git push --force")) {
      return { deny: "force push は使わないでください" } // 本体の代わりに答える
    }
    const r = await next(e) // 後ろのフックと本体を実行
    $.ui.toast("Bash ran") // 結果を見てから反応する
    return r
  }).catch(($, e, next) =>
    next.called ? next(e) : { deny: "ガードが失敗したので止めました" })
}

$

外の世界への唯一の窓口

画面、モデル、ファイル、ストア、タイマー、HTTP など。mod は Node も DOM もない環境で動くので、外に触れるのは必ず $ 経由です。$.ui.toast(…) のように省略せず書きます(読み込み時に静的に棚卸しされるため)。

e

イベントの入力

深く freeze されたプレーンなデータ。直接は書き換えられません。変えたいときは next({ ...e, command }) のようにコピーを渡します。

next

チェーンの残り

next(e) で後ろのフックと Claude Code 本体を実行し、その結果を受け取ります。呼ばずに return すれば、本体の代わりに答えたことになります。

next の 4 つの使い方

  • return next(e) そのまま通す
  • return next({ ...e, text }) 入力を書き換えて通す
  • return { deny: "…" } 本体を動かさずに答える(拒否など)
  • const r = await next(e); … 結果を見てから反応する・結果を書き換える

チェーン.

同じイベントにフックした mod は 5 つの tier に並び、外側から順に呼ばれます。下りで各 mod が e を書き換え、最後に Claude Code 本体(core)が既定の答えを出し、上りで各 mod が結果を書き換えます。外側にいるほど権限が強い と覚えておけば十分です。

  1. prepend 組織ポリシー prependPlugins
  2. user あなたが入れた mod ハッカソンで作るのはここ
  3. append 組織ポリシー appendPlugins
  4. builtin バイナリ同梱の mod sec-default, diff など
  5. core Claude Code 本体 既定の答えを出す

組織は prepend と append の両端を持てます。個人の端末で作る mod は user tier に入ります。

イベント.

on("イベント名", マッチャー?, フック) で受け取ります。各イベントについて「いつ発火するか」と「フックで何を返せるか」をまとめました。マッチャーで対象を絞れます(例: on("tool.call", { tool: "Bash" }, …))。

1 ターンの流れ

  1. session.start 起動
  2. prompt.submit 送信
  3. turn.start ターン開始
  4. turn.step モデルに問い合わせ
  5. tool.call ツール実行前
  6. tool.check 許可判定
  7. ui.render 画面に描く
  8. turn.complete ターン終了
  9. session.end 終了
よく使う まずはここから 観察のみ 結果を変えられない(反応するだけ)

ツール

tool.call よく使う
いつ ツールが実行される直前(許可確認より前)
できること next({ ...e, command }) で入力を書き換え · { deny } で拒否 · { result } で結果を差し替え · await next(e) の後で結果を加工
tool.check
いつ 実行してよいかの判定時(tool.call と PreToolUse の後)
できること { decision: "allow" | "ask" | "deny" }
tool.describe
いつ ツールの説明を初めてモデルに送るとき
できること { description, isDeferred } で説明を書き換え

プロンプト

prompt.submit よく使う
いつ プロンプトが送信されたとき(ターン開始前)
できること next({ ...e, text }) で書き換え · context でモデルだけが読む文脈を追加 · { drop } で破棄
prompt.edit
いつ 入力欄が編集されるたび(キー入力・貼り付け)
できること 挿入される文字や入力欄の中身を書き換え
prompt.fill
いつ 入力欄に下書きが入る直前($.prompt.fill)
できること 文字列やモードを書き換え · { isFilled: false } で入れない
prompt.suggest
いつ 入力欄に薄字の提案が出るとき(Tab で採用)
できること next({ ...e, text }) で書き換え · { isShown: false } で出さない
prompt.autocomplete
いつ 入力中、カーソル位置の単語の補完候補を作るとき
できること { suggestions: [{ text }] } で候補を追加
prompt.mention
いつ @ で指定したファイルを読む直前
できること context を追加 · 別の path を読ませる · { deny }
prompt.compose
いつ システムプロンプトを組み立てるとき
できること { sections } の追加・置き換え・並べ替え・削除
prompt.section
いつ システムプロンプトの名前付きセクションごと
できること { text } で差し替え · { text: null } で削除
prompt.context
いつ 会話ごとに 1 回、最初のメッセージに添える文脈を作るとき
できること { blocks } を追加・削除・書き換え
prompt.attachment
いつ リマインダーなど、本体が自動で足すメッセージごと
できること { text } で差し替え · { text: null } で外す
skill.prompt
いつ スキル本文をモデル向けに展開するとき
できること { text } で差し替え
attribution.text
いつ コミット / PR 文に入れる帰属テキストを作るとき
できること { text } で差し替え

コマンドと設定

command.run よく使う
いつ スラッシュコマンドの実行直前
できること { text } で答える(自作コマンドの応答はここ)· next({ ...e, args }) で引数を書き換え
command.describe
いつ コマンドを補完一覧や /help に出すとき
できること { description, argumentHint, isHidden }
config.set
いつ /config の行が変わる直前
できること { deny } で変更させない · next({ ...e, value }) で値を補正
config.describe
いつ /config の行を一覧に出すとき
できること { label, description, isHidden }

ターン

turn.start
いつ ターン開始時(サブエージェントも。メインは e.agentId なし)
できること 観察のみ
turn.step
いつ モデルへのリクエスト 1 回ごと
できること model / effort を差し替え · 応答ストリームを加工(async generator で書く)
turn.complete よく使う
いつ ターンが終わったとき
できること { text } で回答の下に一行表示(記録は変わらない)

セッション

session.start よく使う
いつ 最初のプロンプトの前と、mod のリロード後(/clear 後は来ない)
できること 観察のみ コマンドやツールの登録、タイマー開始はここで行う
session.end
いつ 終了・/clear・/resume など(e.reason)
できること 観察のみ (全フック合わせて短い時間制限あり)
session.compact
いつ 会話を圧縮する直前
できること 指示やメッセージを書き換え · { skip } で圧縮しない
session.append
いつ 会話に行が保存される直前
できること next({ ...e, message }) で内容を書き換え(マスキングなど)
session.send
いつ 他のエージェント / セッションへメッセージを送る直前
できること 本文・宛先を書き換え · { isDelivered: false } で送らない
session.receive
いつ 外からメッセージが届いたとき
できること next({ ...e, text }) で書き換え · { consumed } で受け取って捨てる
session.attach · detach
いつ スマホなどリモートの画面が接続・離脱したとき
できること 観察のみ
session.measure
いつ ターン後など、使用量の数字が動いたとき
できること 観察のみ ($.session.usage() の値が届く)

サブエージェント

agent.offer
いつ サブエージェントの型をモデルに見せるとき
できること { isOffered: false } で隠す
agent.spawn
いつ サブエージェントやチームメイトの起動・再開の直前
できること next({ ...e, model }) でモデルを変える · { deny }

画面

ui.render よく使う
いつ 各描画箇所を描く直前(→ 描画セクション)
できること 要素ツリーを返す · await next(e) の描画を包む · props を書き換え · next(e) で何もしない
ui.press
いつ 自分が描いた Button が押されたとき
できること next(e) で onPress を実行 · 横取りして別の動作
ui.input · ui.select
いつ Input の入力・確定 / Select の選択
できること 値を書き換えて onInput・onSubmit / onSelect に渡す
ui.focus · ui.scroll
いつ ペインや帯でフォーカス・スクロール位置が動く直前
できること 移動先を変える · { deny } で動かさない
ui.close
いつ ペインが閉じる直前(e.origin.kind: plugin · person · unload)
できること 閉じる前の後片付け
ui.message
いつ 自分の Client が surface.post(data) したとき
できること データを受け取る · { props } で再描画なしに渡し返す
ui.fault
いつ 自分の Client の読み込み・描画・実行が失敗したとき
できること 観察のみ (ログを出してフォールバック)
ui.resolve
いつ 読み込み時、描画面と描画箇所ごとに 1 回
できること 要素テーブルを差し替え(他の mod の要素のスタイルを変える)

メタ

plugin.register
いつ 他の mod が読み込まれる直前
できること { refuse } で読み込ませない(組織の審査用)
engine.create
いつ この mod 用の $ を組み立てるとき
できること $ に名詞を足す(user tier は追加のみ)
telemetry.log · mark
いつ テレメトリが記録される直前
できること 中身を書き換え · { deny }。マッチャーで { to: "collector" } を指定
classic.<Event>
いつ settings の従来フック(PreToolUse など)が動くとき
できること e は従来の stdin JSON。返り値も従来と同じ
<noun>.<method>
いつ 他の mod が $ を呼んだとき(例: fs.read)
できること { deny } · { value } で代わりに答える
*
いつ 上のすべて(telemetry 以外)
できること "tool.*" のような glob も使える

$ API.

$ はフックの第 1 引数で、mod が外の世界に触れる唯一の手段です。$.名詞.動詞(…) の形で、省略せずに書きます。呼び出しそれぞれもイベントなので、他の mod から横取りできます(on("fs.read", …) など)。

画面に出す

$.ui

.toast(text)
画面下の通知バーに数秒表示
.status(text)
ステータス行に表示(undefined で消す)
.log(text)
トランスクリプトに一行足す
.notify(text, { title })
OS のネイティブ通知
.notice(text)
ダイアログの下に一行
.ask(…)
AskUserQuestion のダイアログで質問する
.open({ id, title }) · close({ id }) · panes()
ペインを開く・閉じる・一覧
.focus(…) · scroll(…)
フォーカス・スクロール位置を動かす
.copy({ text })
クリップボードにコピー
.selection()
マウスで最後に選択したテキスト
.invalidate("ui.render")
再描画を依頼($.state なら不要)
.resolve(e)
その画面の要素(Box, Text …)を得る
.blit(…)
再描画なしで Raster / Image を差し替え

モデルとプロンプト

$.model

.complete({ model, prompt })
履歴なしの単発補完。結果は { isAnswered, text }
.fork({ prompt })
この会話の続きとして、ツールなしで 1 回質問
.classify(text, labels)
ラベルを 1 つ選ばせる

$.prompt

.submit(…)
プロンプトを送る(暇になったら新しいターンを始める)
.read()
入力欄の中身を読む
.fill({ text, mode })
入力欄に書く(replace · append · insert)
.suggest(…)
入力欄に薄字で提案
.compose()
システムプロンプトを組み立てて得る
.media({ id })
添付画像・文書の中身(managed の mod のみ)

機能を足す

$.command

.register({ name, description })
/コマンドを追加。応答は command.run で返す
.run(…)
コマンドを入力したように実行
.list()
コマンドの一覧

$.tool

.register({ name, description, inputSchema })
モデルが呼べるツールを追加(mcp__<plugin>__<name>)。応答は tool.call で返す
.call(…)
フックと権限を通してツールを実行
.check(…)
許可判定だけを得る
.list()
今あるツールの一覧

$.agent

.register(…)
サブエージェントの型を追加(<plugin>:<name>)
.spawn({ subagentType, … })
サブエージェントを起動し、終わるまで待つ
.list()
動いているエージェントの一覧

セッションを知る・操作する

$.session

.id() · cwd() · root() · repo()
セッションの識別子と場所
.model() · version() · turns()
モデル · Claude Code のバージョン · ターン数
.messages()
会話(1 メッセージ 1 項目の { role, text, toolUses })
.usage()
コンテキスト使用率 · レート制限 · コスト
.compact() · append({ message }) · send(…)
今すぐ圧縮 · 会話に行を足す · 他セッションへ送る
.surfaces()
接続中の画面(ターミナル · デスクトップ · スマホ)
.authorize()
Anthropic 認証のハンドル($.http.fetch の auth 用。秘密は渡らない)
.identity()
サインイン情報(managed の mod のみ)

$.turn

.abort()
実行中のターンを止める

データを持つ

$.state

.get(ref) · set(ref, value)
セッション中の値。リロードしても残り、描画中に get すると set で自動再描画
.atom · read · update · derive
"claude-code" から import する補助関数

$.store

.get(key) · set(key, value) · delete(key) · keys()
マシン上の全セッションで共有する KV(JSON、合計 4 MiB)

$.fs

.read(path) · write(path, text) · list(path)
ファイルの読み書き(write は非アトミック)
.exists(path) · stat(path) · ancestors(…)
存在確認 · 種類やサイズ · 上位フォルダの指示ファイル

外とつながる

$.http

.fetch(url, init)
ホスト経由の HTTP リクエスト

$.process

.run({ argv }) · spawn({ argv })
コマンドを実行(シェルなし)· 出力をストリームで受ける

$.mcp

.call(…) · connect(…)
接続済み MCP サーバーのツールを呼ぶ · 接続する

その他

$.clock

.now() · sleep(ms) · after(…) · every(…)
時刻とキャンセルできるタイマー

$.audio

.play({ asset }) · speak(text)
mod 内の音声ファイルを再生 · 読み上げ

$.settings · $.config · $.env

.read() · list() / set() · get() / set()
設定の読み出し · /config の行 · 環境変数

$.telemetry · $.plugin

.log() · mark() · name · root
テレメトリ記録 · 自分のプラグイン名とフォルダ

$.state と $.store の使い分け

$.state $.store
寿命 セッションの間(/clear などで初期値) セッションをまたいで残る
ホットリロード 残る 残る
再描画 描画中の get が購読し、set で自動再描画 しない
型の宣言 types/index.d.ts の PluginState に必要 不要
向いている値 画面に出す値 次回の起動でも使う値

引数と戻り値の正確な形は、mod を読み込むと .claude-plugin/types/claude-code/index.d.ts に書き出される型定義が正です。

描画.

on("ui.render", { component: "描画箇所" }, …) で、Claude Code の画面の決まった場所を自分の UI にできます。まず「どこに描けるか」を画面の図で見てから、使える要素と書き方を確認します。

よく使う 4 つの表示

  • A ペイン $.ui.open({ id, title }) + ui.render { component: "Pane" } 全画面表示では横にドック、それ以外はプロンプト欄の上に開く。タブで複数持てる
  • B 帯 ui.render { component: "AbovePrompt" } プロンプト欄の真上の一帯。Button や Input も置ける。全 mod で共有
  • C ステータスライン $.ui.status(text) 画面下の行に短い文字列を出す。undefined で消す。どのフックからでも呼べる
  • D トースト $.ui.toast(text) 通知バーに数秒だけ出す。どのフックからでも呼べる

どこに描けるか

青枠はハッカソンで特に使いやすい 2 か所(AbovePrompt と Pane)です。図はイメージです。

component 画面のどこ 対応
InfoNotice ロゴの下の薄い案内行 ターミナル
UserMessage あなたのプロンプト(> …)、タスク通知、他エージェントからのメッセージ 両方
AssistantMessage Claude の回答テキスト 両方
ToolUse ツール呼び出しの行(Bash(ls -la)) 両方
ToolResult ツール行の下に出る結果 両方
ToolGroup まとめられた行(Read 3 files) 両方
ToolProgress 実行中ツールの進捗行 ターミナル
CommandOutput スラッシュコマンドの出力(自作コマンドの { text } も) 両方
AskUserQuestion 質問ダイアログ 両方
Spinner ターン中に動く行(Sautéing… 12s) 両方
TurnDuration ターンの終わりの行(Baked for 3s) ターミナル
AbovePrompt プロンプト欄の真上の帯。全 mod で共有 両方
PromptHint プロンプト欄の下のヒント行(? for shortcuts) 両方
SessionMode プロンプト欄の下、右端のモード表示 両方
Pane $.ui.open で開くパネル。全画面では横にドック 両方

使える要素

const { Box, Text, Button } = $.ui.resolve(e) で取り出します。画面(e.surface)ごとに使える要素が違います。

要素 用途 主な props 対応
Box 並べる・囲む(flexbox) flexDirection · gap · padding · borderStyle · backgroundColor · hover 両方
Text 文字 color · dimColor · weight · size · mono · italic · wrap 両方
Button 押せるボタン label · hotkey · onPress · variant · plain 両方
Link リンク href · label 両方
Code シンタックスハイライト付きコード / diff source · language · path · format: 'diff' 両方
Markdown 回答と同じ見た目の Markdown text · onLinkPress 両方
Input テキスト入力欄 label · placeholder · value · onInput · onSubmit 両方
Select 選択肢 label · options · value · onSelect 両方
Client 別スレッドで動く描画モジュール(ゲームなど) module · props · width · height 両方
Raster 色付きセルのグリッド(グラフ・ヒートマップ) columns · rows · cells ターミナル
Image 画像(kitty / Ghostty で表示) source · columns · rows · alt ターミナル
Svg SVG source · alt · width · height デスクトップ

書き方は 4 通り

  • 包む const drawn = await next(e) を自分の Box の子に入れて、前後に足す
  • 置き換える next を呼ばずに自分のツリーを返す
  • props を変える next({ ...e, props: { ...e.props, … } }) で本体の描画に渡す値だけ変える
  • 何もしない 描くものがないときは return next(e)

例: ツール結果の下に一行とボタンを足す(包む)

on("ui.render", { component: "ToolResult" }, async ($, e, next) => {
  const { Box, Text, Button } = $.ui.resolve(e)
  const drawn = await next(e) // 本体(と内側の mod)が描いたもの
  return Box({ flexDirection: "column", children: [
    drawn,
    Text({ dimColor: true, children: "↳ reviewed" }),
    Button({ label: "copy", hotkey: "c", onPress: () => $.ui.toast("copied") }),
  ] })
})

ペインを作る(公式サンプル)

コマンド登録 → ペインを開く → 値を書く → 描く、の 4 ステップです。ツール呼び出しを記録してサイドパネルに一覧します。

import { atom, read, update } from "claude-code"
import type { Register } from "claude-code"
import type { ToolCall } from "../types"

const PANE = "tool-calls"
const calls = atom({ plugin: "tool-calls", key: "calls" } as const, [])

export const register: Register = on => {
  // 1. コマンドを登録し、ペインを開く
  on("session.start", async ($, e, next) => {
    await $.command.register({ name: "tool-calls", description: "Show tool calls" })
    void $.ui.open({ id: PANE, title: "Tool calls" })
    return next(e)
  })

  // 2. 自作コマンドに答える
  on("command.run", { command: "tool-calls" }, async $ => {
    await $.ui.open({ id: PANE, title: "Tool calls" })
    return { text: "Tool calls pane opened." }
  })

  // 3. 値を $.state に書く(読んでいる描画が自動で描き直される)
  on("tool.call", async ($, e, next) => {
    await update($, calls, list => [...list, { id: e.tool_use_id, tool: e.tool }].slice(-200))
    return next(e)
  })

  // 4. ペインの中身を描く
  on("ui.render", { component: "Pane", requestId: PANE }, async ($, e) => {
    const { Box, Text } = $.ui.resolve(e)
    const list = await read($, calls)
    return (
      <Box flexDirection="column">
        {list.length === 0 && <Text dimColor>No tool calls yet.</Text>}
        {list.map(call => <Text>{call.tool}</Text>)}
      </Box>
    )
  })
}
// types/index.d.ts(plugin.json の "types" で指す)
export type ToolCall = { id: string; tool: string }

declare module "claude-code" {
  interface PluginState {
    "tool-calls": { calls: ToolCall[] }
  }
}

描画のルール

  • ・要素は $.ui.resolve(e) から取り出す(グローバルにはない)。.tsx なら JSX でも書ける
  • ・Pane と AbovePrompt は e.props.bodyColumns の幅に合わせて描く
  • ・描画中に $.state.get した値は、$.state.set されると自動で描き直される。手動なら $.ui.invalidate("ui.render")
  • ・描画中に $.state.set は呼べない。ボタンの onPress や別のイベントから書く
  • ・検証に通らないツリー(その画面にない要素など)は描かれず、本体の描画に戻る。理由はトランスクリプトに薄字で出る
  • ・描画が出るのはターミナルとデスクトップアプリ(Code タブ)。e.surface で出し分けられる

3 つの型.

作りたいもののほとんどは、次の 3 つのどれか、または組み合わせです。

1. ガード tool.call + { deny }

危険なコマンドや本番への操作を止める。失敗時に素通りしないよう .catch を付けるのが定石です。

export function register(on) {
  on("tool.call", { tool: "Bash" }, async ($, e, next) => {
    if (e.command.includes("git push --force")) {
      return { deny: "force push は使わないでください" } // 本体の代わりに答える
    }
    const r = await next(e) // 後ろのフックと本体を実行
    $.ui.toast("Bash ran") // 結果を見てから反応する
    return r
  }).catch(($, e, next) =>
    next.called ? next(e) : { deny: "ガードが失敗したので止めました" })
}

2. 常駐 UI ui.render + $.state

プロンプト欄の上やサイドパネルに情報を出し続ける。値は $.state に置けば、書き換えるだけで再描画されます。($.state を使う mod は types/index.d.ts で値の型を宣言します)

const turns = { plugin: "turn-counter", key: "turns" }

export function register(on) {
  on("turn.complete", async ($, e, next) => {
    const r = await next(e)
    if (!e.agentId) {
      const { value = 0 } = await $.state.get(turns)
      await $.state.set(turns, value + 1)
    }
    return r
  })

  on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
    const { value = 0 } = await $.state.get(turns) // 描画中の get が購読になる
    if (value === 0) return next(e)
    const { Text } = $.ui.resolve(e)
    return Text({ dimColor: true, children: `このセッション ${value} ターン目` })
  })
}

3. 記憶 prompt.submit + $.store

セッションをまたいで覚えておきたいことを、毎回のプロンプトに文脈として添える。

export function register(on) {
  on("prompt.submit", async ($, e, next) => {
    const notes = await $.store.get("notes").catch(() => undefined)
    if (typeof notes !== "string") return next(e)
    // モデルだけが読む文脈を足す(画面には出ない)
    return next({ ...e, context: [...(e.context ?? []), notes] })
  })
}

はじめかた.

A. Claude に書いてもらう(最短)

  1. Claude Code で /plugin-authoring を実行し、「こういう mod を作って」と頼む
  2. 「Enable hot reloading for this session?」と聞かれたら Enable for this session を選ぶ
  3. ターンが終わると読み込まれる。以降は修正を頼むたびにホットリロード
  4. /plugin で読み込まれているか確認

B. 自分で書く

my-mod/
├── .claude-plugin/
│   └── plugin.json      { "name": "my-mod", "version": "0.1.0", "description": "…" }
├── hooks/
│   ├── hooks.json       { "modules": ["./register.ts"] }
│   └── register.ts      export function register(on, options) { … }
├── types/index.d.ts     $.state を使うときだけ(plugin.json の "types" で指す)
└── tests/*.test.ts      claude plugin test で実行
  • claude --plugin-dir ./my-mod その 1 セッションだけ読み込む。保存するとホットリロード
  • claude plugin validate ./my-mod フックしたイベント、呼んでいる $、拒否される箇所を一覧(--strict --json)
  • claude plugin test ./my-mod *.test.ts を本物のランタイムで実行
  • tsc -p ./my-mod 読み込み後に .claude-plugin/types/ に型が出るので型チェックできる
  • /plugin · /reload-plugins 読み込み中の mod を確認 · 再読み込み

C. 配る

mod のリポジトリに marketplace.json を置けば、そのまま配布できます。

// .claude-plugin/marketplace.json(mod のリポジトリ直下)
{
  "name": "my-mods",
  "owner": { "name": "you" },
  "plugins": [{ "name": "my-mod", "source": "./" }]
}

使う側はターミナルの Claude Code で:

/plugin install my-mod --marketplace <owner>/<repo>

ハマりどころ.

  • 読み込まれない 信頼していないフォルダでは、エラーも出さずに読み込まれません。/plugin で「1 mod active」と出るか確認します。
  • $ は省略せず書く $.ui.toast(…) のように名詞と動詞をそのまま書きます。const ui = $.ui のような書き方や import() は読み込みで拒否されます。
  • 状態の置き場所 保存のたびに register が再実行され、モジュール変数は消えます。画面に出す値は $.state、次回の起動でも使う値は $.store に置きます。
  • 描画中は書けない ui.render の中で $.state.set は使えません。ボタンの onPress や別のイベントから書きます。
  • 時間の上限 フック自身の実行は 1 イベント 10 秒まで(next や $ を待つ時間は数えない)。.catch は 1 秒です。
  • ガードには .catch .catch がないと、フックが例外を投げたときに止めたかった操作がそのまま通ります。
  • 変数名 h JSX はタグを h(…) の呼び出しに変換します。.tsx でローカル変数に h という名前を付けると描画が壊れます。
  • $ の reject await した $ 呼び出しの reject を放置すると mod 全体が外れます。.catch で受けておきます。
  • 名前 plugin.json の name を claude- で始めると validate で弾かれます。
  • 描画が出る場所 描画が出るのはターミナルとデスクトップアプリ(Code タブ)。VS Code のチャットや claude -p ではフックだけが動きます。
  • 組織の端末 Team / Enterprise や管理端末では sec-default が外側に入り、個人の mod が触れない箇所があります。v2.1.299 から /plugin と validate がスキップされたフックを表示します。

アイデア.

何を作るか迷ったときのたたき台です。右側は使う API の目安です。

安全

  • force push・rm -rf・本番 DB 接続を止める

    tool.call + { deny }

  • コミット前に秘密情報をスキャン

    tool.call + 正規表現

  • 本番デプロイだけ 2 段階承認

    tool.call + $.ui.ask

開発体験

  • コミット前に lint / test を自動で通す

    tool.call + $.process.run

  • /pr で差分から PR 説明を生成

    $.command.register + $.model.complete

  • 編集したファイルの影響範囲をパネルに出す

    tool.call + ui.render(Pane)

見える化

  • コスト・コンテキスト残量の HUD

    session.measure + $.ui.status

  • サブエージェントの進捗ダッシュボード

    $.agent.list + ui.render(Pane)

  • CI の状態を常時表示

    $.http.fetch + $.clock.every

体験・遊び

  • 長いタスクの終了を読み上げ / 通知

    turn.complete + $.audio.speak / $.ui.notify

  • 待ち時間のミニゲーム

    ui.render(AbovePrompt)+ Client

  • ツール結果に「セカンドオピニオン」ボタン

    ui.render + Button + $.model.fork

記憶・連携

  • 過去の失敗パターンを毎回の文脈に添える

    prompt.submit + $.store

  • 社内 API をモデルが呼べるツールにする

    $.tool.register + $.http.fetch

  • /ticket で Linear / Jira に起票

    $.command.register + $.http.fetch

最近の変更.

v2.1.296〜v2.1.299 で入った主な変更です。

  • v2.1.299 $.prompt.media(添付画像・文書の中身を読む)と $.session.identity() を追加。どちらも managed settings で入れた mod だけが使えます。
  • v2.1.299 Text に size · weight · mono · tabular を追加(bold は weight に置き換え予定)。Box の余白に xs〜xl の段階指定。
  • v2.1.299 turn.start がサブエージェントとチームメイトのターンでも発火(メインでは e.agentId なし)。agent.spawn が終了したサブエージェントの再開でも発火(e.resume)。
  • v2.1.299 skill.prompt の e にフロントマター・ソース・ファイルパス。session.append に queue の経路。
  • v2.1.299 止められる操作は、フックに渡せないとき通さずに拒否する(fail-closed)。組織管理でスキップされたフックを /plugin と validate が表示。
  • v2.1.296 $.http.fetch の 4 MiB 上限が HEAD を拒否しなくなった(2.1.299 で 204 · 205 · 304 も対象外に)。

API は early access で、リリースごとに変わることがあります。