v2.1.251 and later
You are switching models and want a say before the session changes. PreModelSwitch needs Claude Code v2.1.251 or later. It is not in the 31-event split most of this course’s hooks material draws from. The hooks reference lists it beside PostModelSwitch as the newer pair.
Which switches fire it
The /model command, both model pickers, the Model setting inside /config, fast mode when it changes the session model, and model-change requests from the SDK or Remote Control. Automatic fallback does not fire it, and neither does a session restoring its saved model on resume. PostModelSwitch sees both of those.
Matching on the canonical name
The matcher compares against the target model’s canonical name, the one standard spelling Claude Code reduces every alias to, after ignoring a trailing [1m]. When Claude Code cannot canonicalise the target, every configured hook runs regardless of the matcher. So a handler that blocks specific models has to read to_model itself and not trust the matcher to have done it. The reference does not publish the mapping.
What a switch costs
| Field | What it holds |
|---|---|
from_model, to_model | The current and requested model. |
requested_model | What the user or client asked for, may be empty. |
source | command, picker, or sdk. |
context_tokens | How much context would move to the new model. |
prompt_cache_warm | Whether the existing cache is still warm. |
cache_ttl | 5m or 1h. |
estimated_cache_write_usd | The estimated cost of resending context as a fresh cache write. |
pricing | configured, catalog, or default, where the estimate came from. |
The reference does not state the rate assumed when pricing is default.
Allow, deny, or ask
The JSON answer is a top-level decision: "block" with reason, or hookSpecificOutput with hookEventName: "PreModelSwitch", permissionDecision set to allow, deny, or ask, and permissionDecisionReason. This event rejects defer, updatedInput, and additionalContext. An explicit allow skips the warm-cache confirmation you would otherwise see. ask prompts only on an interactive /model. On every other surface it is a refusal. When several hooks answer, deny outranks ask, which outranks allow, and systemMessage is shown whichever wins.
Exit 2 blocks, and so does a slow hook
Exit 0 proceeds unless the JSON blocks, denies, or asks. Exit 1 shows stderr and applies the switch, with valid JSON still in control. Exit 2 blocks the switch and shows the reason, or stderr, to you. This event’s default timeout is 30 seconds, and a hook cancelled at its timeout is treated as a block.
Your hook calls a pricing service to decide whether the switch is worth it. One afternoon the service is slow.
Every /model is refused. The hook never got as far as writing a reason, because it was killed at 30 seconds, and on this event a timeout is a block.
Keep the decision local, or set a timeout the hook can meet. The cost estimate is already in the payload as estimated_cache_write_usd.
Module 09 lists this event in the blocking inventory from v2.1.251.