◉CLAUDE LABJP
●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 44 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 44 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/Claude Code
⟐ Claude Code/2026-09-02Intermediate

Two hooks that record every model switch and stop the ones you never agreed to

Build a PostModelSwitch hook that logs every model change and a PreModelSwitch hook that blocks unapproved switches during unattended runs. Complete scripts, measured timings, and the reason the two jobs must stay separate.

Claude Code264hooks22PreModelSwitchPostModelSwitchautomation109

✦ Premium Article

Last week my weekly usage on a set of unattended scheduled runs came in higher than I expected, so I went looking for the breakdown. My logs had the outcome of every run and the artifacts it produced. What they did not have, anywhere, was a record of when the session changed models.

As an indie developer running routine work on scheduled automation, splitting work across models is the obvious design. Formatting and search-and-replace go to something light; rewriting prose goes to something heavier. The problem was that this split existed as an intention, not as an observation. I had never once written down what actually happened.

Claude Code v2.1.251 added two hook events for exactly this: PreModelSwitch and PostModelSwitch. We are going to build a recorder and a guard — as two separate hooks, on two separate events. The separation is the interesting part, so let's start there.

A usage total cannot be decomposed after the fact

The Claude Code weekly limit is a shared pool across models. The status line JSON exposes rate_limits.seven_day alongside seven_day_opus and seven_day_sonnet, which tells you the pool carries per-model sub-buckets inside a single ceiling.

Practically, that means any run that used a heavier model than intended has already eaten into the room left for everything else. And from September 14 the weekly limit settles at +25% over the pre-promotion baseline — lower than the +50% in effect now, which makes this a reasonable moment to re-measure how the allocation actually behaves.

Re-measuring needs data. Checking /usage afterwards gives you a total, not an attribution. Some information exists only at the instant the switch happens, so the recorder has to be in place first.

What the two events hand you

PreModelSwitch fires immediately before a switch. PostModelSwitch fires after the session's model has changed. Where other hooks receive things like tool_name, these two receive fields that describe the switch itself.

FieldTypeMeaning
from_modelstringModel in use before the switch
to_modelstringModel being switched to
requested_modelstring | nullThe explicitly requested model; null when the switch was automatic
source"command" | "picker" | "sdk"How the switch was triggered: the /model command, the interactive picker, or the SDK
context_tokensnumberContext size at that moment
prompt_cache_warmbooleanWhether the prompt cache is currently warm
cache_ttl"5m" | "1h"Cache lifetime in effect
estimated_cache_write_usdnumberEstimated cost of the cache write, in USD
pricing"configured" | "catalog" | "default"Which price table produced that estimate

The usual session_id, transcript_path, and cwd come along too. PostModelSwitch carries the same fields, with two additional possible values for source.

That source field is what lets you tell a human choosing a model at a picker apart from a switch that happened inside an unattended run. That distinction becomes the hinge for the guard.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦You will be able to explain, after the fact, which unattended run consumed which model instead of guessing from a single usage total
✦You will avoid the failure mode where a heavy step inside PreModelSwitch quietly stops model switching altogether
✦You will be able to fold the cache write cost reported by estimated_cache_write_usd into a switching decision you were making on per-token price alone
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⟐ Claude Code2026-08-28
Don't let your verification script's full output flow back through a hook
The check was working the whole time. The conversation was what ran out of room. Measured side by side: 58 bytes from one scan of 833 files, 42KB from another. Which paths actually reach the model, and how to enforce a budget on them.
⟐ Claude Code2026-10-15
The Hook That Was Meant to Guard My Japanese Folder Never Blocked Anything — Python Was Reading stdin as cp932
A PreToolUse hook guarding folders like 経理 or 議事録 silently lets edits through when Python's stdin defaults to cp932. How I reproduced it, the three-line fix, and one command to check your own hooks.
⟐ Claude Code2026-10-14
I Set onFailure to block, and the Docs' rm -rf Guard Still Let Everything Through Without jq
When a PreToolUse guard hook fails, does the action go ahead or stop? The docs' own rm -rf example exits 0 and waves everything through when jq is missing. Ten failure modes run for real, the fix, and a script that finds fail-open guards in settings.json.