Every cost tool we own is a rear-view mirror. It tells you what you already spent, after the tokens are gone. Claude Code's new PreModelSwitch hook is the first one that can say no before the money leaves: it fires the instant a session reaches for a pricier model, and it can block that switch. We built a guardrail on it that refuses an upgrade to a premium model once the day's budget is gone, before a single token is billed.
What we built
Version 2.1.251 of Claude Code added two hook events, PreModelSwitch and PostModelSwitch. A hook is just a script the tool runs at a fixed moment; it reads a little JSON about what is happening and can allow it, annotate it, or block it. The interesting one is PreModelSwitch, because it fires before the session changes model and, unusually, it can stop the change dead by exiting with code 2. Nothing else in the cost toolbox can do that. Usage bars, spend dashboards, our own audit log: all of them report after the fact. This is the first place you can put a hand on the wheel rather than read the odometer.
Our guardrail is one small script. It reads the switch (from which model, to which model), sorts models into cost tiers by name (anything with "haiku" is cheap, "sonnet" and "fable" mid, "opus" premium), and applies one rule. A downgrade or a sideways move always passes, no questions. An upgrade into the premium tier is the only thing it gates: it checks the day's spend against a budget you set, and if the budget is gone it blocks the switch and tells the user why. Sort by tier, not by exact model id, and a new model name never needs a code change. Every decision is written to a log, so the guard is also a record of how often the fleet reached for Opus and got told to wait.
A real block looks like this. The switch never happens; the session stays on the cheaper model.
Daily budget $5.00 exhausted ($6.20 spent).
Switch claude-haiku-4-5 -> claude-opus-4-8 blocked.
Why it was worth doing
The whole point of running a fleet on cheap models is that the cheap models do most of the work for pennies. We wrote up last week that ours runs on about nine pence a day. The thing that quietly wrecks that number is the unwatched upgrade: an agent, or a person, flips to the premium model for one hard job and leaves it there, and the bill for the next thousand routine calls is now fifteen times what it should be. You do not notice, because nothing tells you at the moment it matters. You notice on the invoice, which is the one place it is too late to do anything.
A gate at the switch closes that gap. It does not care how the spend was racked up or which agent did it; it only asks one question at the one moment the answer can change the outcome. And it is built to get out of the way. The single rule we would not bend is that the guard fails open: bad input, a missing config, an unknown model, any bug at all, and it allows the switch and exits clean. A cost guard that bricks the fleet on its own mistake is worse than the overspend it was meant to stop, so it only ever says no when it is certain, and says yes to everything else. The test harness pins that down on purpose: feed it garbage and it still exits 0.
The honest catch
We could not fire a live model switch to test this, because our own fleet is still on 2.1.241 and the PreModelSwitch event does not exist there yet. So we did the next honest thing rather than claim a demo we could not run. We built the guard to the exact input schema the 2.1.251 docs publish, and wrote a harness that drives it with real PreModelSwitch payloads and checks both the decisions and the actual process exit codes: the block exits 2, the allow exits 0, garbage fails open. Fourteen checks, all green. When the fleet upgrades, the guard is a two-line entry in settings.json away from live, and on any older build the hook simply never fires, so nothing breaks and nothing is enforced. The gap is real; we would rather name it than paper over it.
What's now in the stack
cost_guardrail.py: a PreModelSwitch hook that blocks an upgrade into the premium tier once the day's budget is spent, and fails open on anything it is unsure about.cost_policy.json: tiers, list prices, daily budget and an allowlist, all editable for your own gateway rates.- A test harness driving the real hook payload, asserting exit code 2 blocks and exit 0 allows. Fourteen checks, no live switch required.
- A standalone copy on GitHub. Steal what you like.