QR Send

スマホとPCのChrome間で、URLやテキストをQRコード1回のペアリングで送り合える自作Chrome拡張。アカウント不要・無料。

使ってみる →

Claude Codeにモデル切替専用フック`PreModelSwitch`/`PostModelSwitch`が来た:切り替わる瞬間を止められるようになった

Selenium・自動化

Claude Codeをオートモードで走らせていると、気付いたらモデルが変わっていることがあります。長めのタスクを投げっぱなしにして戻ってきたら、いつの間にかOpusからSonnetに切り替わっていた。逆に、軽い作業のつもりが重いモデルに切り替わっていて「あれ、こんな判断まで任せていたっけ」となる。私はオートモードがデフォルトになった話を書いたときにこの感覚を味わいました。

以前Hooksの記事で、フォーマット・危険コマンドのブロック・完了通知の3パターンを紹介しました。今回はその続編です。v2.1.251(2026年8月28日)で、モデル切り替え専用のHookイベントPreModelSwitchPostModelSwitchが追加されました。「何のイベントが取れるか」ではなく、「モデルが切り替わる瞬間」という1点だけに絞って見ていきましょう。

モデル切り替えだけを狙ったイベントが要る理由

既存のイベント一覧を見ると、モデルの情報を持っているのはSessionStartだけでした。それも公式docsによると「modelフィールドは常に含まれるわけではない」とのことで、セッション開始時点のスナップショットに過ぎません。セッション中に/modelコマンドやオートモードの判断でモデルが変わっても、それを追いかける手段が今までありませんでした。

環境変数で拾えないかと思って調べたのですが、公式docsに正直な注記がありました。$CLAUDE_MODELという環境変数は存在しません。$ANTHROPIC_MODELを自分で設定していたとしても、セッション中に/modelで切り替えた時にその値は追従しないそうです。つまり今までは、セッション中のモデル変更を確実に検知する方法自体がなかったことになります。PreModelSwitchPostModelSwitchは、まさにこの穴を埋めるために作られたイベントです。

PreModelSwitch: 切り替わる前に止められる

ここからが本番です。PreModelSwitchは、モデル切り替えが実際に適用される前に発火します。自分が/modelで指定した場合だけでなく、クライアント側からの切り替え要求も含みます。ここが重要なのですが、公式docsには「切り替えをブロックできる」とはっきり書かれています。

入力として渡ってくるのはfrom_modelto_modelです。他のイベントのように単一のmodelフィールドではなく、切り替え前後の両方を受け取れるのが今回のイベントの核心だと思います。

{
  "hooks": {
    "PreModelSwitch": [
      {
        "matcher": "claude-opus-5",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/confirm_switch.py"
          }
        ]
      }
    ]
  }
}

matcherには、公式docsの表現をそのまま借りると「切り替え先のto_modelから導き出したcanonical name」を指定します。claude-opus-5のような単体指定だけでなく、claude-opus-4-6|claude-opus-5のような正規表現も使えるとのことでした。ただしcanonical nameがどう導出されるかの対応表までは、私が確認した範囲の公式docsには載っていませんでした(今後増補されるかもしれません)。

ブロックの中身は、既存のPreToolUseと同じ発想です。終了コード2で終了すれば、その切り替え自体が止まります。「このプロジェクトでは高コストなモデルへの切り替え前に必ず一声確認したい」というような使い方ができそうです。試しに、Opus系への切り替えだけ止めるスクリプトを書いてみましょう。

import json, sys

data = json.load(sys.stdin)
from_model = data.get("from_model", "")
to_model = data.get("to_model", "")

if "opus" in to_model.lower():
    print(
        f"opus系への切替をブロックしました: {from_model} -> {to_model}",
        file=sys.stderr,
    )
    sys.exit(2)

sys.exit(0)

これでopusという文字列を含むモデルへ切り替わろうとした瞬間だけ止まります。標準エラー出力の内容は、そのままClaudeに「なぜブロックされたか」として渡ります。そのまま別のモデルで作業を続けるか、そこで止まって私に聞き直すか、Claude自身の判断材料になります。

タイムアウトすると、切り替え自体がキャンセルされる

ここは注意です。PreModelSwitchのフックがタイムアウトでキャンセルされた場合、そのモデル切り替え自体がブロックされる仕様になっています。他の多くのイベントは、タイムアウトしても処理自体は素通りすることが多いのですが、PreModelSwitchは違います。フックが重い処理を挟んでいると、意図せずモデル切り替えを止めてしまうことになります。

デフォルトのタイムアウトは30秒です。公式docsによると、commandhttpmcp_toolタイプのフックは、UserPromptSubmitPreModelSwitchPostModelSwitchの3つだけデフォルトが30秒に下げられているそうです。他のイベントより短めに設定されているとのことでした。確認スクリプトを書くなら、外部APIを叩くような重い処理は避けましょう。ローカルで完結する軽い判定にとどめるのが安全だと思います。

PostModelSwitch: 変わった事実を記録する

PostModelSwitchは、セッションのモデルが実際に変わった後に発火します。自分が明示的に切り替えた場合だけでなく、Claude Code自身が判断して変えた場合(オートモードの挙動や、セッションを再開したときにモデルを復元する処理など)も含まれるのが、このイベントの守備範囲の広さだと感じました。

こちらは非同期イベントなので、ブロックはできません。ログを残す・通知を飛ばす、といった記録目的の用途になります。

{
  "hooks": {
    "PostModelSwitch": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/log_model_switch.py"
          }
        ]
      }
    ]
  }
}
import json, sys, datetime

data = json.load(sys.stdin)
from_model = data.get("from_model", "unknown")
to_model = data.get("to_model", "unknown")

with open(".claude/model_switch.log", "a") as f:
    f.write(f"{datetime.datetime.now().isoformat()} {from_model} -> {to_model}\n")

sys.exit(0)

これでセッション中のモデル変更が全部ログに残ります。「このタスクは結局どのモデルで実行されたのか」を後から追えるようになります。オートモードでコストや品質を気にしている人には地味に効くはずです。–restrictedの記事でも書きましたが、Claude Codeは「見えないところで自動的に判断してくれる」機能が増えていく方向にあります。その判断を後から検証できる手段があるかどうかは、実務で使う上でけっこう大事だと思っています。

2つのイベントの違いを整理する

PreModelSwitch PostModelSwitch
発火タイミング 切り替えが適用される前 切り替えが完了した後
ブロック可否 できる できない(非同期イベント)
デフォルトタイムアウト 30秒 30秒
タイムアウト時の挙動 切り替え自体がキャンセルされる 特に影響なし(記録目的のため)
向いている用途 確認・ブロック ログ・通知・監査

同じv2.1.251では、SessionStartのresumeフックにもセッションの古さ(staleness)や再キャッシュコストの推定値が渡るようになったそうです。モデル切り替えの追跡といい、こちらといい、「セッションの状態をフックからもっと詳しく覗けるようにする」という方向の改善がまとまって入ったリリースだったようです。

正直に書いておくこと

今回調べていて、公式docsだけでは埋まらなかった部分が2つありました。1つはcanonical nameの対応表です。「to_modelから導出する」とは書かれているものの、具体的にどのモデル名がどのcanonical nameになるかの一覧は見つけられませんでした。もう1つは、二次情報のリリースまとめ(releasebot.ioやgradually.aiなど)に出てくる「確認・注釈も可能」という記述です。確認(confirm)や注釈(annotate)という言葉が、公式docsのJSON出力仕様のどのフィールドに対応するのかまでは特定できませんでした。実装する際は、まず終了コード2によるブロックだけを試して、手元の環境で挙動を確認してみてください。

まずはPostModelSwitchのログから試す

ブロックまでいきなり組むと、タイムアウトの仕様もあって事故りやすいと感じました。まずはPostModelSwitchでログを残すところから試してみましょう。実害がなく、オートモード運用の実態を見える化できるので、Hooksに慣れる意味でもちょうど良い入り口だと思います。慣れてきたらPreModelSwitchで、コストの高いモデルへの切り替えだけ確認を挟む、という順番がいいはずです。

というわけで、モデル選択そのものがブラックボックスになりがちな中で、「いつ、何から何に切り替わったか」を自分の手元に記録できるようになったのは地味ですが大きい変化だと思います。他にも便利な使い方があれば、コメントで教えてもらえると嬉しいです。

コメント

タイトルとURLをコピーしました