0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code 2026年10月アップデート——Modsでツール呼び出しをTypeScriptで横取りできるようになった

0
Posted at

3行まとめ

  • 2026年10月1日のClaude Code v2.1.287でModsが入った。プラグインに同梱するJavaScript/TypeScriptの関数で、ツール呼び出し・プロンプト送信・UI描画などのイベントを観察・書き換え・横取りできる。/diffパネルもすでにModとして実装し直されている
  • 最小構成は3ファイル。Node.jsもビルドも不要で、claude plugin validateで「どのイベントに何をするか」を静的に一覧でき、claude plugin testでセッションなしに単体テストまで回せる。手元のv2.1.296で実際に動かして確認した
  • Modsはサンドボックスなしで自分の権限のまま動き、ツール呼び出しを承認することもできる。入れる前にvalidateで中身を確認すること、ガード用のフックは.catchで「失敗したら拒否」に倒すことが重要。ほかにアイドル時の自動コンパクション(v2.1.299)なども入った

Claude Codeの10月は、Modsという大きめの新機能から始まった。

これまでClaude Codeの拡張手段は、settings.jsonに書くhooks、Skills、MCPサーバー、ステータスラインなどがあった。どれも「Claude Codeの外側」からスクリプトを走らせたり、Claudeにテキストやツールを渡したりする仕組みだ。ModsはClaude Codeのプロセスの内側で動く関数で、ツール呼び出しの前に割り込んで拒否したり、スピナーの表示を書き換えたり、独自のパネルを描いたりできる。

前月の9月アップデート記事で取り上げた/diffパネルも、今はcc-plugin-diffという組み込みModとして動いている。

既存の拡張手段との違い

公式ドキュメントの比較表を要約するとこうなる。

Mod settings hook Skill MCPサーバー
実体 プラグイン内の関数(Claude Codeのプロセス内) シェルコマンド・HTTP・プロンプト SKILL.md 外部プロセス
変えられるもの ツール呼び出し、プロンプト、コマンド、ターン、UI描画 ツール呼び出しの可否・引数・結果、追加コンテキスト Claudeの知識・手順 Claudeが使えるツール
UIを描ける ○ × × ×
書くもの JavaScript / TypeScript スクリプト+settings.json Markdown 任意の言語

なお用語が紛らわしくなっていて、ドキュメント上はModのイベントハンドラを「hook」、従来のsettings.jsonに書くほうを**「settings hook」**と呼び分けている。

最小のModを作って動かしてみた

公式チュートリアルの「ツール呼び出し回数を数える」例を少し変えて、ツール名ごとの回数を/tallyで表示するModを書いてみた。ファイル構成は3つだけ。

tool-tally/
├── .claude-plugin/
│   └── plugin.json
└── hooks/
    ├── hooks.json
    └── register.js

plugin.jsonは普通のプラグインのマニフェスト。

tool-tally/.claude-plugin/plugin.json
{
  "name": "tool-tally",
  "version": "0.1.0",
  "description": "ツール呼び出しをツール名ごとに数えて /tally で表示する",
  "author": { "name": "Your Name" }
}

hooks.jsonにmodulesキーでコードのパスを書く。このキーがあることがModである条件になっている。

tool-tally/hooks/hooks.json
{
  "description": "tool-tally hooks module",
  "modules": ["./register.js"]
}

本体のregister.js。Modが読み込まれるとregister(on)が呼ばれ、on(イベント名, ハンドラ)でフックを登録する。

tool-tally/hooks/register.js
// ツール名ごとの呼び出し回数
const counts = new Map()

export function register(on) {
  on('session.start', async ($, e, next) => {
    await $.command.register({ name: 'tally', description: 'ツール別の呼び出し回数を表示' })
    return next(e)
  })

  on('tool.call', async ($, e, next) => {
    counts.set(e.tool, (counts.get(e.tool) ?? 0) + 1)
    return next(e)
  })

  on('command.run', { command: 'tally' }, async () => {
    if (counts.size === 0) return { text: 'まだツール呼び出しはない' }
    const lines = [...counts].sort((a, b) => b[1] - a[1]).map(([t, n]) => `${t}: ${n}`)
    return { text: lines.join('\n') }
  })
}

ハンドラが受け取るのは3つ。

  • $: Mods API。コマンド登録($.command)、UI($.ui)、プロセス実行($.process)など、Mod の外に作用する操作はすべてここを経由する
  • e: イベントの中身。tool.callならe.toolがツール名、e.commandがBashのコマンド、のように引数がフィールドになっている
  • next: 次のハンドラ(他のModか、最終的にはClaude Code本来の処理)を呼ぶ関数

ハンドラの書き方で3通りの振る舞いになる。next(e)をそのまま返せば観察、next({...e, 変更})なら書き換え、nextを呼ばずに結果を返せば横取り(本来の処理は走らない)。上のコードだとtool.callは観察、command.runは横取りだ。

読み込みは--plugin-dirで1セッションだけ有効にできる。ディレクトリを監視していて、保存するとホットリロードされる。リロードのたびにregisterが呼び直されるので、countsのようなモジュール内の変数はリセットされる点には注意。コードは.tsで書いても、ビルドなしでそのまま読み込まれる。

claude --plugin-dir ./tool-tally

validate で「何をするModか」を静的に確認できる

Modsで良くできていると思ったのがclaude plugin validate。コードを実行せずに、どのイベントをフックしていて、どのMods APIを呼んでいるかを静的解析で一覧にしてくれる。手元(v2.1.296)で上のModに実行した結果のうち、hooks部分がこれ(先頭にはマニフェストの検証結果も出る)。

$ claude plugin validate .
Validating hooks: /private/tmp/modtest/tool-tally/hooks/hooks.json

  ❯ ./register.js hooks: session.start, tool.call, command.run{command=tally}
  ❯ ./register.js answers its own command: command.run{command=tally}
  ❯ ./register.js gating hook without .catch: tool.call
  ❯ ./register.js calls: $.command.register

✔ Validation passed with warnings

hooks:がフックしているイベント(マッチャーは波括弧内)、calls:が呼んでいるMods APIの一覧。Modが外部に作用する手段は$経由に限られているので、この一覧を見れば「ファイルを読むか」「プロセスを起動するか」「ネットワークに出るか」が実行前に分かる。この静的解析を成立させるため、$を変数に代入したり分割代入したりするとvalidateで弾かれる、という制約もある。

警告のgating hook without .catch: tool.callは後述する。

セッションなしで単体テストが回る

もう一つ便利なのがclaude plugin test。claude-code/testingからテスト用のAPIをimportして、イベントを擬似的に発火させられる。

tool-tally/tests/tally.test.ts
import { expect, test } from 'claude-code/testing'

test('ツール名ごとに集計される', async ($, on) => {
  // ツール呼び出しにはClaude Codeの代わりに即答し、実際には何も実行しない
  on('tool.call', () => ({ result: 'ok' }))
  await $.tool.call({ tool: 'Read', file_path: 'a.md' })
  await $.tool.call({ tool: 'Bash', command: 'ls' })
  await $.tool.call({ tool: 'Read', file_path: 'b.md' })
  const answer = await $.command.run({ command: 'tally', args: '' })
  expect(answer.text).toBe('Read: 2\nBash: 1')
})
$ claude plugin test
tests/tally.test.ts:
(pass) ツール名ごとに集計される [99.53ms]

 1 pass
 0 fail
Ran 1 test across 1 file. [0.74s]

サインインもネットワークも不要で、1秒かからずに終わる。settings hookにはイベントを擬似的に発火させる公式のテストランナーがなかったので、ここは明確に進化している。

ガード用のフックは .catch で「失敗したら拒否」に倒す

Modsで一番実用的なのは、危険なコマンドを止めるガードだと思う。公式ドキュメントの例を借りると、force pushを拒否するフックはこう書ける。

on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
  if (/git push .*--force/.test(e.command)) {
    // nextを呼ばずに返すので、コマンドは実行されない
    return { deny: 'Force pushes are not allowed in this repository. Push to a new branch instead.' }
  }
  return next(e)
})

denyのテキストはツールの結果としてClaudeに渡るので、「代わりに何をすべきか」を書いておくとClaudeがそのまま軌道修正する。

ここで先ほどの警告が効いてくる。フックが例外を投げたりタイムアウトしたりした場合、nextを呼ぶ前だったならそのフックはスキップされ、次のハンドラが代わりに走る。つまりガードのコード自体がバグって落ちると、止めたかったコマンドが素通りする(fail-open)。

これを防ぐのがon(...)の戻り値に付ける.catch。

on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
  // guardがすでにnextを呼んでいたなら、その結果を返す
  if (next.called) return next(e)
  // まだなら拒否に倒す
  return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
})

なお.catchのハンドラ自体にも短い制限時間があり、そこでも失敗するとフックは素通りに戻る。ハンドラはなるべく単純に保つ。

validateのgating hook without .catchは、「このtool.callフックは呼び出しを止められる位置にいるのに、失敗時の扱いが決まっていない」という指摘だ。数えるだけのtool-tallyなら落ちても素通りで構わないので無視してよいが、ガード目的のフックでは必ず.catchを付けると覚えておきたい。

入れる前に知っておくべき権限モデル

公式ドキュメントにはっきり書かれているが、Modはサンドボックスなしで、ユーザー自身の権限で動く。読み込まれたModは次のことができる。

  • ユーザーとしてファイルの読み書き、プロセス起動、ネットワーク通信
  • 環境変数や設定ファイルに置いたAPIキーの読み取り
  • すべてのプロンプトとツール呼び出しの閲覧・書き換え
  • ユーザーに確認される前にツール呼び出しを承認する
  • プランやAPIキーでモデルを呼び出す(使用量を消費する)

Bashのサンドボックスを有効にしていても、Modが起動したプロセスはその外で動く。なお、パーミッションの確認ダイアログの表示内容だけはModから書き換えられない。ただしModは確認ダイアログが出る前に承認できるので、歯止めとしては限定的だ。

運用上の対策は次のとおり。

  • インストール前にclaude plugin validateでhooks:とcalls:を確認する
  • インストールしたModを1セッションだけ止めるなら--safe-mode(Mod以外のカスタマイズも止まる)、恒久的に止めるなら~/.claude/settings.jsonに"disableAllHooks": true(settings hookとカスタムステータスラインも止まる)。どちらも組み込みModは止めない
  • managed settingsがある環境やTeam / Enterpriseでサインインしている環境では、組み込みのガードcc-plugin-sec-defaultがチェーンの先頭で読み込まれ、組織が管理する設定をユーザーのModから守る。管理者はこのガードのオプションallowManagedModsOnlyで組織指定のModだけに絞ることもできる

その他の10月の変更

Mods以外で、日常的に効きそうなものを拾っておく。

  • アイドル時の自動コンパクション(v2.1.299): ターミナルで200Kトークン超の会話を約54分放置すると、1時間のプロンプトキャッシュが切れる前に自動でコンパクションする。戻ってきたときに巨大な会話を全部再処理せずに済む。しきい値はCLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS、無効化は"idleCompaction": false。v2.1.302からは/configの「Idle compaction」でも切り替えられる
  • Haiku 5.5がデフォルトのHaikuに(v2.1.293): Anthropic API経由ならHaiku指定はclaude-haiku-5-5になる。詳細はHaiku 5.5の記事で書いた
  • Agentツールにeffortパラメータ(v2.1.292): サブエージェントごとに思考量を指定できる
  • auto modeの分類器モデルを指定(v2.1.302): CLAUDE_CODE_AUTO_MODE_MODELでクライアント側のauto mode分類器が使うモデルを選べる
  • 単体のClaude Designは12月14日まで(v2.1.302): /design-sync・/design-loginに、claude.ai/designが2026年12月14日で閉じる旨の通知が出るようになった

まとめ

  • v2.1.287で入ったModsは、プラグインに同梱するJS/TSの関数で、ツール呼び出し・プロンプト・UIに割り込める。/diffパネルもすでにModになっている
  • 3ファイル・ビルド不要で書け、claude plugin validateで何をするModかを静的に確認、claude plugin testでセッションなしに単体テストできる。手元でも数分で動いた
  • Modはサンドボックスなしで自分の権限で動き、ツール呼び出しを勝手に承認することもできる。入れる前にvalidate、ガード用フックには.catchでfail-closedに、の2点は押さえておきたい
  • 「settings hookのシェルスクリプトで頑張っていたガード」は、今後はModで書き直す価値がある

ぱんだツールズ では PDF・画像・CSV・テキスト処理などの開発者向けツールを 110 個以上公開中。全部無料・登録不要・ブラウザ完結で使える。Claude Codeで開発した個人開発プロダクトの実例として、よかったら覗いていって。
https://sakutto-panda.com


この記事は Zenn にも同じ内容を投稿しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?