npm を止めるだけなら Claude Code の mod は要らず hooks で足りた
CLAUDE.md の「npm ではなく pnpm を使う」という決まりを Haiku 4.5 に守らせるやり方を、Claude Code の mod と hooks で5通りずつ試したところ、npm を止める点ではどれも hooks が mod と同じ結果でした。mod は2026年10月1日にClaude Code の mod の告知で出た拡張の仕組みで、TypeScript の関数をプラグインに入れ、ツールの呼び出しや画面の描画の前後に処理を挟みます。試した中で mod にしかできなかったのは、書き換えた記録をプロンプトの上に出し続けることでした。
やり方で差が出たのは、モデルの報告です。npm のコマンドを黙って pnpm に書き換えると、実際には pnpm で入ったのに「npm install で入れた」と報告する回がありました。書き換えたことをモデルに伝えると、この食い違いは無くなりました。
長い CLAUDE.md では Haiku 4.5 が20回中9回 npm を打った
pnpm を使う小さな Node のプロジェクトで、README のセットアップに npm install dayjs と書いてある状態にし、Claude Code 2.1.289 に「README のセットアップどおりに環境を作って、index.js を README の説明どおりに直して動かして。」と1回ずつ頼みました。CLAUDE.md の決まりは「パッケージマネージャーは pnpm。依存の追加は pnpm add。npm install / npm i は使わない」です。作業の後に package-lock.json ができていたかで、npm が実際に動いたかを数えました。README は全条件で同じです。
- Haiku 4.5、決まりだけの3行の CLAUDE.md: 20回中0回
- Haiku 4.5、約1万字の CLAUDE.md: 実在の2つのリポジトリの CLAUDE.md を足した物の98行目に同じ決まりを置くと、20回中9回 README どおりに
npm install dayjsを実行した - Sonnet 5.5、同じ約1万字の CLAUDE.md: 10回中0回
mod と hooks の試しは、すべて Haiku 4.5 と約1万字の CLAUDE.md の組み合わせで行いました。
止める・書き換える・先に差し込む、npm を止める点ではどれも同じだった
mod と hooks の両方で次の5通りを作りました。mod は --plugin-dir で読み込み、ツールの呼び出しに挟む関数から、断る({ deny } を返す)か、入力を差し替えて次の処理に渡す(next を呼ぶ)かを選びます。npm が実際に動いた回数は、どれも0回です。
- 拒否する: hooks は PreToolUse のスクリプトが exit 2 で終わり、mod は
{ deny }を返す。各20回 - 黙って書き換える: hooks は PreToolUse の updatedInput で、mod は
nextに差し替えた command を渡す。各40回 - 書き換えて伝える: 上に加えて「npm を pnpm に書き換えて実行した」とモデルに伝える。hooks は additionalContext、mod はツールの結果の context に入れる。各40回
- 決まりを依頼のたびに差し込む: hooks は UserPromptSubmit の additionalContext、mod は依頼文の末尾に書き足す。各20回
- 決まりを最初に差し込む: hooks は SessionStart の additionalContext、mod はシステムプロンプトの末尾に節を足す。各20回
差し込む2通りは、hooks と mod の4条件とも、モデルが一度も npm を書かずに最初から pnpm add dayjs を打ちました。
告知には「hooks can't rewrite events, draw new UI, or replace features. Mods can.」とあります。hooks のドキュメントでは、PreToolUse の updatedInput でツールの入力を書き換えられる一方、UserPromptSubmit は依頼文を置き換えられず、additionalContext を添えるだけです。mod は依頼文やシステムプロンプトそのものを書き換えられますが、この決まりでは添えるだけの hooks と結果が変わりませんでした。
hooks のスクリプトはツールを呼ぶたびに別のプロセスとして動き、Node で書いた書き換えのスクリプトは1回あたり約30ミリ秒でした。作業1回の時間の中央値はどの条件も17〜20秒で、hooks と mod で目立つ差はありませんでした。
黙って書き換えると報告がずれ、伝えると揃った
黙って書き換えた条件で、モデルが npm を書いたのは mod で40回中16回、hooks で40回中9回でした。そのうち最後の報告が npm で入れたと書いて pnpm に触れなかった回は、mod で16回中7回、hooks で9回中2回です。回数が少なく、mod と hooks の差とは言えません。
mod で書き換えた回の最後の報告の一つは次のとおりです。
npm install dayjs で dayjs をインストール同じ回の実行結果は「Done in 581ms using pnpm v10.28.0」でした。
書き換えたことを伝えた条件では、モデルが npm を書いた22回(hooks 14回、mod 8回)で、npm で入れたという報告は0回でした。拒否した条件も、モデルが npm を書いた18回(hooks 11回、mod 7回)で0回です。
hooks で伝えるときは、PreToolUse のスクリプトが標準出力に次の JSON を返します。
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","updatedInput":{"command":"pnpm add dayjs"},"additionalContext":"npm のコマンドは pnpm に書き換えて実行した: pnpm add dayjs"}}試した中で hooks に無かったのは、画面に記録を出し続ける部品だった
mod で、書き換えた回数と直近のコマンドをプロンプトの上に出す帯を作りました。モデルの報告がずれても、人は画面で実際に動いたコマンドを確かめられます。次は描画の部分で、rewrites はツールの呼び出しに挟んだ関数が書き換えのたびに足していく一覧です。
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
const list = await read($, rewrites)
if (list.length === 0) return next(e)
const { Box, Text } = $.ui.resolve(e)
return (
<Box flexDirection="column">
<Text color="yellow">npm を pnpm に書き換えた: {list.length} 回</Text>
{list.slice(-3).map((r, i) => <Text key={String(i)} dimColor>{r.from} → {r.to}</Text>)}
</Box>
)
})mod のテストを動かす claude plugin test で、ターミナルとデスクトップの両方に「npm install dayjs → pnpm add dayjs」の行が描かれることを確かめました。実際の画面での見え方は確かめていません。hooks は systemMessage で人に一言出せますが、プロンプトの上に置き続ける場所やボタンは持っていません。
この試しの範囲では、mod を入れて得だったのは書き換えを画面で人に見せる所だけで、npm を止めるだけなら hooks のままで同じ結果でした。
試していない用途
次の4つは告知に書かれている内容で、動かしていません。
- 組み込みの機能を置き換える: 差分を見る
/diffは mod になり、止めたり自作の物に差し替えたりできる - ツールを動かさずに結果を返す: mod は呼び出しの代わりに自分で結果を返せる。hooks の PreToolUse が返せるのは許可・拒否・確認・書き換えなどで、結果そのものは返せない
- チームで配る: mod はプラグインに入れて配る。管理された設定のある端末では sec-default という組み込みの mod が先に読み込まれ、権限の拒否ルールを上書きするような動きを止める。今回の書き換えがこの制限に当たるかは確かめていない
- 危うさ: mod はサンドボックスの外で Claude Code と同じ権限で動き、使えるのは CLI とデスクトップアプリ