Hook event 33 of 33

PostModelSwitch

Requires Claude Code v2.1.251 or later and runs after the session model changes so a hook can attach model-specific context to a later request.

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

The other half of the v2.1.251 pair

You already know PreModelSwitch as the gate. This is what runs after the switch lands. Like its partner, it needs Claude Code v2.1.251 or later. The two share most of a payload and the same matcher rule. They fire at different moments, and only one of them can refuse.

More switches than the gate sees

PreModelSwitch only sees switches you or your client asked for. PostModelSwitch fires after those, and after three kinds the gate never sees: automatic fallback that changes the session model, the model-setting transitions of entering and leaving plan mode, and a session restoring its saved model on resume. It does not fire when a fallback-chain model serves one turn without becoming the session’s model. source adds auto and resume to the values PreModelSwitch uses. requested_model is null for auto and holds the restored setting for resume.

The rest of the payload is the PreModelSwitch set: from_model, to_model, context_tokens, prompt_cache_warm, cache_ttl, estimated_cache_write_usd, and pricing. The matcher follows the same canonical-name rule, and the hooks reference publishes neither the mapping nor the rate behind pricing: "default".

Context arrives with the next request

Here is the part that trips people up: your hook runs after the model has changed, but the text you return does not land in the conversation right away. The JSON answer is hookSpecificOutput with hookEventName: "PostModelSwitch" and additionalContext, and plain stdout on exit 0 is context too. Either way the text is queued for the next request Claude Code sends, not injected on the spot. If the hook is still running five seconds after that prompt goes out, the request goes without it and the output attaches to the request after. If several switches happen before any request, only the output for the last target is delivered. The default timeout is 30 seconds.

The model has already changed by the time the hook runs. Exit 2 cannot undo that, and its stderr goes to you only. Exit 1 cannot either, with valid JSON still applied.

The mistake most people make first

You switch models twice in a row before typing anything, then send a prompt. Your hook wrote a log line both times.

Only the context for the second target arrives. The first hook’s output was dropped when the second switch happened, and the log shows two runs and one delivery. Nothing else reports the drop.

Write the context for the model the session ends up on, and read to_model rather than assuming the run you are in is the one that will be delivered.