Hook event 32 of 33

PreModelSwitch

Requires Claude Code v2.1.251 or later and runs before a user- or client-requested model switch so a hook can allow, deny, or ask about the change.

Exit code veto
Exit 2 blocks
Matchers
canonical target model name after stripping a [1m] suffix
if filter
Does not apply
Since
v2.1.251
Docs checked
2026-08-29

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

FieldWhat it holds
from_model, to_modelThe current and requested model.
requested_modelWhat the user or client asked for, may be empty.
sourcecommand, picker, or sdk.
context_tokensHow much context would move to the new model.
prompt_cache_warmWhether the existing cache is still warm.
cache_ttl5m or 1h.
estimated_cache_write_usdThe estimated cost of resending context as a fresh cache write.
pricingconfigured, 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.

The mistake most people make first

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.