Claude Media
PreModelSwitch hookとPostModelSwitch hookでモデル切り替えを制御する

PreModelSwitch hookとPostModelSwitch hookでモデル切り替えを制御する

v2.1.251のPreModelSwitch/PostModelSwitchフックが、どの切り替えで走り、何を受け取り、どう止められるか。switchModelsOnFlagやfallbackModelとの境界も扱います。

Claude Code v2.1.251は、モデルの切り替えに反応するPreModelSwitchとPostModelSwitchの2つのフックイベントを追加しました。ただし、どちらも「モデルが変わる場面すべて」で走るわけではありません。Preは人間やクライアントが要求した切り替えだけ、Postはセッションのモデルが実際に変わった場面すべてが対象です。この差が、フックで何を実現できるかを決めます。

Claude Codeのhooks全体の中でも、この2つは入力にコスト見積もりが含まれる点が珍しいイベントです。以下、v2.1.286の手元環境と公式のhooks・model-config・errorsの各ページを突き合わせて書いています。

どの切り替えでどちらのフックが走るか

最初に押さえたいのは、「モデルが変わった」ことと「PreModelSwitchが走る」ことは同じではない点です。PreModelSwitchは、次の要求を受けて切り替えを適用する直前に走ります。

  • /model <name>と/modelのピッカー
  • Option+PまたはAlt+Pのモデルピッカー
  • /configのModel設定
  • モデルが変わるfast modeの有効化
  • Agent SDKのホストやRemote Controlからのset_model、またはapply_flag_settingsでのモデル変更

一方、Claude Codeが自分で行う切り替えにはPreModelSwitchが走りません。自動フォールバックやセッション再開時のモデル復元がこれにあたり、PostModelSwitchだけに届きます。

切り替えの種類PreModelSwitchPostModelSwitch
/model・ピッカー・/config・fast mode・SDK/Remote Controlの要求PreModelSwitch走る(止められる)PostModelSwitch走る
分類器による自動モデルフォールバックPreModelSwitch走らないPostModelSwitch走る
opusplanでプランモードに出入りしたときPreModelSwitch記載なしPostModelSwitch走る
セッション再開時のモデル復元PreModelSwitch走らないPostModelSwitch走る
fallbackModelチェーンが1ターンだけ代役を務める場合PreModelSwitch走らないPostModelSwitch走らない

fallbackModelチェーンの代役はそのターン限りで、セッションのモデルは変わりません。そのためPostModelSwitchも走りません。可用性起因のフォールバックの設定そのものはClaude Code fallbackModelで過負荷に備えるに詳しく書いています。

サブエージェントのモデル解決は、公式のPre/Post両方の一覧に出てきません。解決順はv2.1.251で変わっています。以前はCLAUDE_CODE_SUBAGENT_MODELが先頭で、呼び出しごとの指定やfrontmatterのmodelを上書きしていました。現在は呼び出しごとのmodel、frontmatterのmodel、CLAUDE_CODE_SUBAGENT_MODEL、メイン会話のモデルの順です。

PreとPostの役割分担

2つのイベントは、止められるかどうかで性格が分かれます。

くらべる

PreModelSwitchとPostModelSwitch

適用の直前

PreModelSwitch

人間かクライアントが要求した切り替えを、適用前に止める・確認させる・通すイベントです。同期的に走り、既定のタイムアウトは30秒です。使えるフック型はcommand・http・mcp_toolの3つで、prompt型とagent型は動きません。

変更の直後

PostModelSwitch

モデルが変わった後に走り、止めることはできません。標準出力かadditionalContextが、切り替え後の次のリクエストでClaudeの文脈に入ります。モデル別の追加指示を流し込む用途です。

マッチャーは両者で共通です。切り替え先モデルの正規名([1m]サフィックスを除く)と照合されます。そのためopusというエイリアスでも日付付きID、Bedrock用のIDでも、解決先が同じならclaude-opus-5の1行で拾えます。claude-opus-4-6|claude-opus-5のような|区切りや、.*opus.*のような正規表現も書けます。

正規名を特定できないモデルIDには注意が必要です。LLMゲートウェイだけが知るカスタムIDなどは、マッチャーに関係なくすべてのPreModelSwitchフックが走ります。ブロックを目的にするフックは、マッチャーに頼らず入力のto_modelを自分で見る必要があります。

フックが受け取る入力で何が分かるか

PreModelSwitchは、共通フィールドに加えて次のものを標準入力のJSONで受け取ります。

入力

入力フィールドの4つのまとまり

  • 切り替えの前後

    from_modelとto_modelです。requested_modelは要求が名指しした値で、opusのようなエイリアスか完全なID、既定モデルへの切り替えならnullが入ります。

  • 要求の出どころ

    sourceがcommand(/model <name>・/config・fast mode)、picker(ピッカー)、sdk(SDKまたはRemote Control)のどれかを示します。

  • 再送されるコンテキスト

    context_tokensは、次のリクエストが再送するトークン数です。直前の応答の入力・キャッシュ読み取り・キャッシュ作成・出力を合算した値で、最初の応答の前は0です。

  • キャッシュ関連

    prompt_cache_warm・cache_ttl(5mか1h)・estimated_cache_write_usd・pricingの4つです。

estimated_cache_write_usdは、context_tokensをto_modelのキャッシュに書き込む費用の見積もりで、次の応答自体の費用は含みません。サーバー側で全文を再キャッシュしなくて済む場合もあるため、公式も「見積もり」として扱っています。pricingは、組織が独自レートを設定していればconfigured、定価ならcatalog、価格が未知のモデルではdefaultです。

モデルごとにキャッシュが別だという前提は、プロンプトキャッシュのページに書かれています。/modelで切り替えると、次のリクエストは会話履歴の全体をキャッシュなしで読みます。仕組みの全体像はプロンプトキャッシュの基礎にまとめてあります。

入力の例として、公式はSonnet 5で/model opusを実行した場合を挙げています。

{
  "hook_event_name": "PreModelSwitch",
  "from_model": "claude-sonnet-5",
  "to_model": "claude-opus-5",
  "requested_model": "opus",
  "source": "command",
  "context_tokens": 182340,
  "prompt_cache_warm": true,
  "cache_ttl": "5m",
  "estimated_cache_write_usd": 1.1396,
  "pricing": "catalog"
}

18万トークン超の会話で切り替えると、約1.14ドルの再キャッシュが発生する見積もりです。フックがこの数字を使えば、切り替えの直前に金額を見せられます。

止める・確認させる・通す

判断の返し方は3通りです。exit code 2または最上位のdecision: "block"で切り替えを取り消します。細かく制御するなら、hookSpecificOutputのpermissionDecisionにallow・deny・askのどれかを返します。

返し方動き
allow動き切り替えを通す。キャッシュが温かいときに出る確認も省略される
deny動き切り替えを取り消す。permissionDecisionReasonがユーザーへの理由になる
ask動きユーザーに確認を求める。理由は確認プロンプトに表示される

defer・updatedInput・additionalContextは受け付けません。複数のフックの判断が割れたときの優先順はdeny、ask、allowの順です。

askの確認画面を出せるのは、対話セッションの/modelだけです。非対話の-p実行、/config、set_modelでは、askは拒否として扱われます。社内で一律にaskを返すフックを入れると、SDK経由の切り替えが全部失敗する点は見落としやすい箇所です。

もう1つ、PreToolUseとは逆向きの規則があります。PreModelSwitchは、タイムアウトまでに応答しなかったフックが切り替えをブロックします。PreToolUseでは、command・http・mcp_tool型のフックがタイムアウトしてもツール呼び出しは通ります(Agent SDKのコールバックフックは逆にブロックします)。exit codeが0でも2でもなく、JSONの判断も返さない場合はブロックされず、stderrが表示されて切り替えは適用されます。

systemMessageは判断にかかわらずユーザーに表示されます。コスト通知だけを出したいフックは、{"systemMessage": "..."}を返してexit 0で終われば足ります。

手元で動かして確かめる

公式のブロック例は、Opus 4.6への切り替えだけを止めるものです。macOS/Linux版は、jqで入力のto_modelを見てexit 2を返します。

{
  "hooks": {
    "PreModelSwitch": [
      {
        "matcher": "claude-opus-4-6",
        "hooks": [
          {
            "type": "command",
            "command": "jq -e '.to_model | test(\"opus-4-6\")' > /dev/null && { echo 'Opus 4.6 is retired for this project. Use a newer model.' >&2; exit 2; }; exit 0"
          }
        ]
      }
    ]
  }
}

Windowsでは、PowerShellスクリプトをargs付きで呼ぶ形が公式に載っています。commandにpowershell.exe、argsに-NoProfile・-ExecutionPolicy Bypass・-Fileとスクリプトのパスを並べます。スクリプトの中では$hookInput.to_modelを見ます。

実際にモデルを切り替えなくても、フックのコマンド部分だけは標準入力にJSONを流して試せます。v2.1.286の環境で、commandの中身をそのままシェルに渡しました。

$ echo '{"hook_event_name":"PreModelSwitch","to_model":"claude-opus-4-6"}' \
  | (jq -e '.to_model | test("opus-4-6")' > /dev/null \
     && { echo 'Opus 4.6 is retired for this project. Use a newer model.' >&2; exit 2; }; exit 0)
Opus 4.6 is retired for this project. Use a newer model.
$ echo $?
2
$ echo '{"hook_event_name":"PreModelSwitch","to_model":"claude-opus-5"}' \
  | (jq -e '.to_model | test("opus-4-6")' > /dev/null \
     && { echo blocked >&2; exit 2; }; exit 0)
$ echo $?
0

claude --helpでもv2.1.286には--bareの説明があり、「フックを含めてスキップする最小モードで起動する」旨が書かれています。フックが効いていない疑いがあるときは、このフラグ付きで起動していないかを最初に見ます。

手順

設定したPreModelSwitchフックの確かめ方

  1. 1

    フックを登録する

    .claude/settings.jsonに上の設定を書きます。マッチャーを付けるなら、切り替え先の正規名で書きます。

  2. 2

    別のモデルのセッションで切り替える

    Opus 4.6以外のモデルで起動したセッションから、/model claude-opus-4-6を実行します。

  3. 3

    ブロックの表示を確認する

    セッションのモデルは変わらず、PreModelSwitchフックがブロックしたこととフックの書いた理由が表示されます。

切り替えがブロックされたときのメッセージと直し方

ブロックされると、次の形式のメッセージが出ます。

Model switch to Opus 4.6 was blocked by a PreModelSwitch hook: Opus 4.6 is retired for this project. Use a newer model.

コロンより後ろが原因を示します。errorsページが挙げる原因は次の5種類です。

  • フックが書いた理由: denyかaskの理由が表示されている。その指示に従うか、フックが許すモデルを選ぶ
  • did not respond before its timeout: フックが時間内に答えなかった。コマンドの停止を直すか、そのフックのtimeoutを延ばす
  • confirmation required, and this session cannot ask: askが理由なしで返され、確認画面を出せない面から要求された。対話セッションの/modelから切り替える。-p実行の/modelでは、理由のあとに(run /model interactively to confirm)が付く
  • so organization-managed PreModelSwitch hooks could not be checked: 組織の管理プラグインが配るフックを確認できなかった。確認できないままでは適用しない仕様で、失敗が解消すれば次の試行から通る
  • a PreModelSwitch hook failed before answeringまたはPreModelSwitch hooks were cancelled (the control stream closed) before answering: フックが判断を返す前に終わった、または制御ストリームが閉じて取り消された。判断なしは承認とみなされないため、claude --debugで原因を見て再試行する

SDKやRemote Controlが発端の切り替えでは、メッセージに切り替え先のモデル名が入りません。v2.1.260より前は、管理プラグインの確認失敗が「プラグインのフックを読み込めない」という別の文言で出ていました。組織が管理プラグインを使っていない場合でも、セッション内で以降の切り替えが拒否され続けたと、errorsページは書いています。

PostModelSwitchでモデル別の指示を入れる

PostModelSwitchの使い道として、公式は組織全体の指示をモデルごとに出し分ける例を挙げています。CLAUDE.mdを書き換えずに、Opusに切り替わったときだけ「実装作業はサブエージェントに任せ、この会話は計画とレビューに使う」と伝える構成です。

{
  "hooks": {
    "PostModelSwitch": [
      {
        "matcher": ".*opus.*",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'On Opus, delegate implementation work to subagents and keep this conversation for planning and review.'"
          }
        ]
      }
    ]
  }
}

入力のフィールドはPreModelSwitchと同じで、sourceにauto(自動フォールバックなどClaude Code自身の変更)とresume(再開時の復元)が加わります。autoではrequested_modelがnull、resumeでは復元された保存済みのモデル設定が入ります。

配信のタイミングにも癖があります。次のプロンプトを送った時点でフックが5秒以内に終わっていなければ、その要求は出力なしで送られ、出力は次の要求に回されます。モデルが何度か続けて変わったときは、最後の切り替え先に対する出力だけが届きます。標準出力がClaudeの文脈に入るイベントは、UserPromptSubmit・UserPromptExpansion・SessionStartとこのPostModelSwitchの4つです。他の多くのイベントは標準出力をデバッグログに書くだけです。

PostModelSwitchは止められないので、exit code 2を返してもstderrがトランスクリプトにエラー通知として出るだけで、Claudeにも見えず、セッションは進みます。

switchModelsOnFlagとfallbackModelとの境界

分類器のフォールバックに関する既存設定はswitchModelsOnFlagです。falseにすると、フラグされたリクエストでセッションが一時停止し、フォールバックモデルへ切り替えるか、プロンプトを編集して同じモデルで再試行するかを選べます。

新しいフックとの関係は、一見すると重なって見えます。実際には、PreModelSwitchは自動フォールバックに走らないので、フックで分類器の切り替えを拒否することはできません。

switchModelsOnFlagfallbackModelチェーンPreModelSwitchPostModelSwitch
対象switchModelsOnFlag分類器による再実行fallbackModelチェーン過負荷・利用不可時の代役PreModelSwitch要求された切り替えPostModelSwitch変わったセッションのモデル
持続switchModelsOnFlag切り替えるか選ぶfallbackModelチェーン1ターン限りPreModelSwitch適用前に判断PostModelSwitch変更後に記録・指示
止められるかswitchModelsOnFlag一時停止で選択させるfallbackModelチェーン設定した順に試すPreModelSwitch止められるPostModelSwitch止められない

分類器による切り替えに対してフックでできるのは、切り替え後のPostModelSwitchで記録するか、モデル別の指示を足すことです。切り替え自体を管理したいならswitchModelsOnFlagが窓口になります。

どの面倒をフックに任せるか

ここまでの仕様から、実運用での使い分けは次のようになります。

  • 承認済みモデル以外への切り替えを止めたいなら、PreModelSwitchのブロックが向きます。組織の許可リストとしてavailableModelsも使えますが、こちらは固定の一覧で判定する仕組みです。フックは任意のスクリプトで判断できるため、入力のcontext_tokensが大きい会話でだけ確認を挟む、といった条件も書けます
  • 切り替えの金額を見せたいだけなら、systemMessageを返してexit 0にすれば足ります
  • 自動フォールバックやセッション再開を含めてモデルの変遷を残したいなら、PostModelSwitchしか選べません

フックを入れる代償は保守です。スクリプトを持つ以上、jqなどの依存、タイムアウト、SDKからのaskの扱いを運用側が面倒を見ます。個人利用では、/modelのキャッシュ確認だけで足りる場面が多そうです。

よくある質問

キャッシュ確認を出さずに切り替えさせたい

allowを返します。/modelが出すキャッシュ温存中の確認が省略されます。確認自体のルールは、キャッシュが温かく、切り替え先が直前の応答を出したモデルではない場合に限られるとプロンプトキャッシュのページにあります。

まとめ

PreModelSwitchは「人間が要求した切り替えを適用前に止める」ための、PostModelSwitchは「モデルが実際に変わった後に記録や指示を足す」ための道具です。自動フォールバックを止めたい場合はswitchModelsOnFlag、変遷を残したい場合はPostModelSwitchと、切り替えの種類で窓口を選びます。

hooksの全体像はClaude Code Hooks完全ガイド、コピペで使えるレシピはClaude Code Hooks実例カタログにあります。コマンド実行そのものを封じるClaude Codeの制限モード(--restricted)の使い方は、v2.1.248で入った運用側の制御です。

この記事を共有:XはてブLinkedIn