Claude Mods(以下 mod)は、TypeScript か JavaScript の関数で Claude Code の振る舞いと画面を変える仕組みで、2026-10-01 リリースの Claude Code v2.1.287 で追加されました1 。ツール呼び出しを止める・書き換える、出力に混じった API キーを伏せる、権限の判断を差し替える、プロンプトの上に自分の表示を出す、といったことが 1 つのプラグインでできます。この記事では、筆者が実際に作って claude plugin validate・claude plugin test・claude -p で動作を確かめた mod を題材に、作り方、hooks・スキルとの違い、安全に入れる手順を 2026 年 10 月時点の公式ドキュメントに沿って説明します。
mod は、Claude Code がイベントを起こすたびに呼ばれる関数をまとめたプラグインです2 。イベントには、ツール呼び出し(tool.call)、プロンプト送信(prompt.submit)、画面の描画(ui.render)などがあります。関数はイベントの前・後・代わりに動けるので、次の 3 通りの扱い方ができます。
見るだけ(Observe) : 記録して next(e) でそのまま通す
書き換える(Rewrite) : 変えたコピーを next({ ...e, ... }) に渡す。結果を書き換えることもできる
代わりに答える(Answer) : next を呼ばずに結果を返す。ツールは実行されない
Claude のツール呼び出しを mod の tool.call hook が受け、危険なら deny を返し、通すなら権限チェックとツール実行のあとに結果の API キーを伏せて Claude に返す流れ 図: 筆者作成(Claude Code 公式ドキュメントの Mods ガイドをもとに作図)
公式ブログは、mod でできることとして次の 4 つを挙げています1 。
プロンプトをモデルに届く前に書き換える
ツール呼び出しを止める・書き換える・やり直す
権限の要求を承認・拒否する
ツールの出力から秘密情報を伏せてから Claude に読ませる
さらに、ペイン(横のサイドバー)やプロンプト上の帯、ボタン、入力欄を描いたり、ツールの行やスピナーなど Claude Code 自身の表示を描き直したりもできます2 。
公式ドキュメントでは、mod の中のイベントハンドラーを「hook」、settings.json に書く従来の hooks を「settings hook」と呼び分けています2 。この記事でも同じ呼び方をします。settings hook の基本は Claude Code の hooks 入門 で扱っています。
Anthropic は 2026-10-01 に mod を発表しました。ブログでは、hooks で一部の制御はできたものの、hooks ではイベントの書き換え、新しい UI の描画、組み込み機能の置き換えができなかった、と理由を説明しています1 。
組み込み機能も mod になった : / diff のペインや AGENTS.md の読み込みは、Claude Code に組み込まれた mod として動いています。/ plugin から無効にしたり、自分の版に置き換えたりできます2
配り方はプラグインと同じ : mod はプラグインの中に入れて配るので、マーケットプレイスや / plugin install がそのまま使えます(プラグインとマーケットプレイス )
更新が続いている : v2.1.288(10-02)と v2.1.289(10-03)でも mod の描画やテストに関する修正が入っています3
mod には Claude Code v2.1.287 以降が必要で、既定で有効です2 。筆者の環境は次のとおりでした。
claude --version
いちばん手軽なのは、対話セッションで「プロンプトの上に今のブランチ名を出す mod を作って」のように頼む方法です4 。Claude は組み込みの plugin-authoring スキルを使い、~/ .claude/ dev-mods/ <セッション ID>/ の下に mod を書きます。
最初のファイルを保存したとき、このセッションでホットリロードを有効にするか聞かれる
有効にすると、ターンの終わりに mod が読み込まれ、変更のたびに再読み込みされる
~/ .claude は保護されたパスなので、default や acceptEdits モードでは各ファイルの作成を承認する
作った mod はそのセッションでしか読み込まれません。残したいときは、ディレクトリを ~/ mods/ などに移して claude --plugin-dir で読み込みます4 。
ここからは、筆者が作った safety-mod を例に、自分で書く手順を説明します。この mod は次の 4 つを行います。
rm -rf・git push --force・git reset --hard を実行させずに理由を返す
Bash の出力から API キーや TOKEN= の値を *** に置き換えてから Claude に渡す
止めた件数をプロンプトの上の帯に出す
/ safety コマンドで集計を表示する
mod は普通のプラグインに hooks/ hooks.json の modules を足したものです4 。
safety-mod/
├── .claude-plugin/
│ └── plugin.json
├── hooks/
│ ├── hooks.json
│ └── register.ts
└── tests/
└── safety-mod.test.ts
.claude-plugin/ plugin.json はプラグインのマニフェストです。名前を claude- で始めると、Anthropic の名前に似ているとして validate で落ちます4 。
{
"name" : "safety-mod" ,
"version" : "0.1.0" ,
"description" : "Blocks destructive shell commands, masks secrets in Bash output, and adds /safety" ,
"author" : { "name" : "Skill We Find" }
}
hooks/ hooks.json の modules に、コードのファイルを 1 つ書きます。これがあるとプラグインが mod になります。
{
"description" : "safety-mod hooks module" ,
"modules" : [ "./register.ts" ]
}
Claude Code は mod を読み込むとき、ファイルが export する register を呼び、on を渡します。on(イベント名, 絞り込み, 関数) で hook を登録します5 。ビルドは不要で、.ts をそのまま読み込みます4 。
import type { Register } from 'claude-code'
const DANGEROUS = /\brm\s+-[a-z]*r[a-z]*f|\brm\s+-[a-z]*f[a-z]*r|\bgit\s+push\b.*(--force|\s-f\b)|\bgit\s+reset\s+--hard\b/
const SECRETS = [
/AKIA[0-9A-Z]{16}/g ,
/sk-(ant-)?[A-Za-z0-9_-]{20,}/g ,
/((?:API_KEY|SECRET|TOKEN|PASSWORD)[A-Z_]*=)(?!\*\*\*)\S+/g ,
]
let blocked = 0
let masked = 0
function mask (text : string ): string {
let out = text
for (const re of SECRETS ) {
out = out.replace (re, (m, prefix ) => {
masked += 1
return typeof prefix === 'string' ? prefix + '***' : '***'
})
}
return out
}
export const register : Register = (on ) => {
on ('session.start' , async ($, e, next) => {
await $.command.register ({ name : 'safety' , description : 'safety-mod が止めた数と隠した数を表示する' })
return next (e)
})
on ('tool.call' , { tool : 'Bash' }, async ($, e, next) => {
if (DANGEROUS .test (e.command )) {
blocked += 1
$.ui.invalidate ('ui.render' )
return { deny : 'safety-mod: このコマンドは止めています。削除や強制 push が必要ならユーザーに確認してください。' }
}
const result = await next (e)
if (result.deny || result.isError || !result.result ) return result
const { stdout, stderr } = result.result
return { result : { ...result.result , stdout : mask (stdout ?? '' ), stderr : mask (stderr ?? '' ) } }
})
on ('ui.render' , { component : 'AbovePrompt' }, async ($, e, next) => {
if (blocked === 0 ) return next (e)
const { Text } = $.ui.resolve (e)
return Text ({ bold : true , children : [`safety-mod: 危険なコマンドを ${blocked} 件止めました` ] })
})
on ('command.run' , { command : 'safety' }, async () => {
return { text : `止めたコマンド ${blocked} 件 / 伏せた秘密情報 ${masked} 件` }
})
}
コードの読みどころは次のとおりです。
{ tool: 'Bash' } は matcher : Bash の呼び出しだけで hook が動く。配列や正規表現も書ける5
{ deny } を返すと止まる : next を呼ばないので、権限の確認もツールの実行も起きない。deny の文は Claude がツールの結果として読むので、次にどうすればよいかを書く5
await next(e) のあとで結果を書き換える : ツールが実行されたあとの stdout を置き換えて返す。Claude は伏せたあとの文字だけを読む
$.ui.invalidate('ui.render') で再描画 : 件数が変わったら画面を描き直させる5
mods API は $.名前空間.メソッド と省略せずに書く : const ui = $.ui のように変数に入れると validate で落ちる4
--plugin-dir で読み込むと、Claude Code が実行中のバージョンの型定義を .claude-plugin/ types/ に書き出します。筆者の環境では「Written by Claude Code 2.1.289.」と書かれた型定義が生成され、npx tsc -p . --noEmit で register.ts の型エラーが 0 件になることを確かめました。イベントや API は版ごとに変わりうるので、公式も手元の型定義を最優先するよう書いています4 。
claude plugin test は、セッションもログインもネットワークも使わずに、イベントを流し込んで hook の動きを確かめます6 。on('tool.call', ...) の「スタブ」が Claude Code の代わりにツールの結果を返します。
import { expect, test } from 'claude-code/testing'
test ('rm -rf は実行させない' , async ($, on) => {
on ('tool.call' , () => ({ result : { stdout : 'should not run' , stderr : '' } }))
const r = await $.tool.call ({ tool : 'Bash' , command : 'rm -rf ./build' })
expect (r.deny ).toContain ('safety-mod' )
})
test ('出力の API キーを伏せる' , async ($, on) => {
on ('tool.call' , () => ({ result : { stdout : 'ANTHROPIC_API_KEY=sk-ant-abcdefghijklmnopqrstuvwxyz\nok' , stderr : '' } }))
const r = await $.tool.call ({ tool : 'Bash' , command : 'cat .env.example' })
expect ((r.result as { stdout : string }).stdout ).toBe ('ANTHROPIC_API_KEY=***\nok' )
})
実際のファイルには git push --force と / safety のテストを足して、全部で 4 本にしました。
作った mod は、次の 3 段で確かめます。
claude plugin validate で mod が登録する hooks と calls を一覧し、claude plugin test で 4 件のテストが通り、claude -p で cat の出力の DEMO_TOKEN が *** に置き換わる様子 筆者が実行した出力を録画(2026-10-05、Claude Code 2.1.289)。validate と test の出力は一部の行を省略
claude plugin validate ./ safety-mod : コードを実行せずに読み、hooks: 行に登録したイベント、calls: 行に使う mods API を出す。イベント名の打ち間違いはここでエラーになる4
claude plugin test : 4 件とも pass。失敗すると終了コード 1 になるので CI に組み込める6
claude -p ... --plugin-dir ./ safety-mod : 本物のセッションで確かめる。claude -p "/ safety" --plugin-dir ./ safety-mod は safety-mod: 止めたコマンド 0 件 / 伏せた秘密情報 0 件 を返した
3 段目では、さらに 2 つを確かめました。rm -rf build を頼むと、ツールの結果が safety-mod: このコマンドは止めています。… になり、コマンドは実行されませんでした。APP_NAME=demo と DEMO_TOKEN=hello-world-123 を書いたファイルを cat させると、Claude が見せた出力は DEMO_TOKEN=*** でした。
帯(AbovePrompt)の描画は、validate と型チェックは通っていますが、筆者は対話ターミナルでの見た目までは確かめていません。claude -p は画面を描かないためです2 。
--plugin-dir で読み込んだディレクトリは監視され、ファイルを保存するとホットリロードされます。再読み込みで register が呼び直されるので、let の値は 0 に戻ります4 。
公式ドキュメントにある例をもとに、開発での使いどころを整理します。
場面 使うイベント やること 危険な操作の前に人に聞く tool.call + $.ui.askrm -rf などで実行を保留し、「Run it / Refuse」を選ばせる5 ブランチによって push を拒否 tool.checkmain にいるときだけ git push を deny にする5 PR の話題に文脈を足す prompt.submitプロンプトに PR とあれば現在のブランチ名を Claude にだけ渡す5 トークンとキャッシュの監視 turn.stepリクエストごとのキャッシュ読み込み量を記録する5 CI の状況を見る ui.render(Pane)パイプラインの状態をペインに出す(公式ブログのチーム向け例)1
tool.check は、権限ルールと settings hook の判断のあとに動くイベントです。next(e) が返す allow / ask / deny を受けて、別の判断に差し替えられます5 。
公式の比較表をもとに、選び方をまとめます2 。
mod settings hook スキル MCP サーバー 正体 Claude Code のプロセス内で動く関数 イベントで実行するシェルコマンド・HTTP など Claude が読む SKILL.md ツールを提供する外部プロセス 変えられるもの ツール呼び出し・プロンプト・コマンド・ターン・画面 ツール呼び出しやプロンプトの可否、引数と結果、追加の文脈 Claude の知識と手順 Claude が使えるツール 画面に描ける できる できない できない できない 書くもの JavaScript / TypeScript スクリプトと settings.json Markdown 任意の言語のサーバー 選ぶとき ペインや帯、独自コマンド、イベントの書き換えが欲しい 手持ちのスクリプトで止める・許す・記録する 同じ指示を何度も貼っている 外部システムにつなぎたい
筆者の判断基準は次のとおりです。
止めるだけなら settings hook : シェルスクリプトで書けて、mod の API の変化を追わなくてよい
結果の書き換えや画面が要るなら mod : 出力のマスクや、件数・状態の表示は mod でないとできない
手順を覚えさせたいならスキル : Claude Code のスキルの作り方 を参照
mod は Claude Code の中で、あなたと同じ権限で動きます。サンドボックスには入りません2 。公式が挙げる「mod が届く範囲」は次のとおりです。
ファイルの読み書き、プログラムの起動、ネットワーク通信
環境変数と settings ファイル(中の API キーを含む)
すべてのプロンプトとツール呼び出しを見て、書き換える
権限の確認が出る前にツール呼び出しを承認する
あなたのプランや API キーでモデルを呼ぶ
そのうえで、筆者は次の順で入れています。
入れる前に claude plugin validate で中身を一覧する : リポジトリを clone して実行する。calls: に $.fs.write・$.process.run・$.http.fetch・$.env.get・$.model.complete があれば、その理由をコードで確かめる7
信頼できる作者・マーケットプレイスからだけ入れる : マーケットプレイスの許可リストなど、プラグインの制御がそのまま mod にも効く7
まず --plugin-dir で 1 セッションだけ試す : 気に入ったら / plugin install 名前@マーケットプレイス で入れる2
おかしいと思ったら止める : 1 つだけなら / plugin で無効化。全部なら claude --safe-mode で起動するか、~/ .claude/ settings.json に "disableAllHooks": true(settings hook も止まる)2
Team・Enterprise プランでログインしている場合や、管理設定(managed settings)がある端末では、組み込みの sec-default という mod がユーザーの mod より先に読み込まれます7 。これにより、ユーザーの mod は deny ルールで拒否された呼び出しを承認できず、管理設定の PreToolUse hook のブロックも覆せません。
一方で、ask ルールや管理設定以外の PreToolUse hook のブロックは、ユーザーの mod が承認で上書きできます7 。つまり、自分の settings hook で止めているつもりでも、承認する mod を入れれば通ってしまいます。また、Read(.env) を拒否していても、mod 自身の $.fs.read は止まりません7 。管理者がユーザーの mod を一切読み込ませたくない場合は、管理設定で allowManagedModsOnly を有効にします。
mod の hook には時間制限があり、超えるとその hook は飛ばされます8 。安全のための hook が時間切れで飛ばされると、止めたかったコマンドが実行されるので注意してください。
Claude Mods の主な時間制限(秒、v2.1.289 時点) データを表で見る Claude Mods の主な時間制限(秒、v2.1.289 時点) 制限 上限(秒)(秒) hook 1 回の実行 10 .catch ハンドラー 1 session.end の hook 合計 1.5 $.process.run(既定) 30
出典: Claude Code 公式ドキュメント Mods reference の Limits(2026-10-05 参照)。hook の時間には next や mods API の中で待つ時間を含まない
ほかにも、$.fs.read / $.fs.write は 1 ファイル 4 MiB まで、$.store は JSON で合計 4 MiB まで、$.model.complete の maxTokens は既定 1024 です8 。
イベント名の打ち間違いです。'tool.calls' のような名前は "tool.calls" is not an event で落ちます。イベント名は変数ではなく文字列リテラルで書きます4 。
hook が例外や時間切れで失敗すると、Claude Code はその hook を飛ばして次へ進みます。止める用途の hook には .catch を付け、失敗時も { deny } を返して閉じる側に倒します5 。
on ('tool.call' , { tool : 'Bash' }, guard).catch (async ($, e, next) => {
return { deny : 'The command guard failed, so this command was not run: ' + next.error .kind }
})
筆者の環境では、最初に claude plugin test を実行したとき「the rollout switch was saved off by an earlier session」と表示されました。メッセージの指示どおり、ネットワークにつながる状態で claude を 1 回起動してから再実行すると、テストが動きました。
インストールしたプラグインは版ごとにキャッシュされます。開発中は --plugin-dir でディレクトリを直接読み込み、配るときに version を上げます4 。
Claude Mods は 2026-10-01 の v2.1.287 で追加された、Claude Code の中で動く TypeScript / JavaScript のイベントハンドラー
tool.call で止める・書き換える、await next(e) のあとで出力を伏せる、ui.render で画面に描く、が同じファイルで書ける
claude plugin validate → claude plugin test → --plugin-dir の順に確かめると、セッションを汚さずに開発できる
mod はあなたの権限で動き、サンドボックスに入らない。入れる前に validate の calls: を読む
止めるだけなら settings hook、出力の書き換えや画面が要るなら mod、と使い分ける
Claude Code v2.1.287(2026-10-01 リリース)以降で、既定で有効です。claude --version で確かめ、古ければ更新します2 3 。
hook はターミナル、Desktop アプリの Code タブ、VS Code 拡張のチャット、claude -p、Agent SDK のどれでも動きます。ただし、ペインや帯などの描画が出るのはターミナルと Desktop アプリだけです2 。
不要です。Claude Code は .js や .ts をそのまま読み込みます。エディターで補完や型チェックをしたい場合は、--plugin-dir で読み込んだときに生成される .claude-plugin/ types/ を使います4 。
turn.step でリクエストごとに next({ ...e, model }) としてモデルを差し替えたり、agent.spawn でサブエージェントのモデルを選んだりできます8 。セッション全体の切り替えは / model を使うのが簡単です。
claude --safe-mode で起動すると、入れた mod がすべて止まります。原因の mod が分かったら / plugin の Installed タブで無効化かアンインストールします2 。