Claude Code hook: halt the session when the model is downgraded mid-session (e.g. Fable 5 -> Opus 4.8) A developer created a hook for Claude Code that halts a session when the model serving it is downgraded mid-session, such as from Fable 5 to Opus 4.8. The hook reads the transcript to detect model changes and stops the session to prevent silent usage on an unintended model. A hook that halts a Claude Code session when the model serving it changes mid-session — for example when Fable 5 falls back to Opus 4.8 because credits ran out or a safety classifier flagged a request. Without this, a session can silently continue for tens of minutes on a model you did not choose. In the case that prompted this, a session ran 309 messages on Fable 5, switched at the 310th, and continued for 91 more messages over 30 minutes before anyone noticed. mkdir -p .claude/hooks curl -o .claude/hooks/model-guard.sh \ https://gist.githubusercontent.com/jimmc414/6e7d61bfe2af477b30b444802f64dbf2/raw/model-guard.sh chmod +x .claude/hooks/model-guard.sh Register it in .claude/settings.json or ~/.claude/settings.json for all projects : { "hooks": { "UserPromptSubmit": {"hooks": {"type": "command", "command": "${CLAUDE PROJECT DIR}/.claude/hooks/model-guard.sh"} } , "PreToolUse": {"hooks": {"type": "command", "command": "${CLAUDE PROJECT DIR}/.claude/hooks/model-guard.sh"} } } } Requires jq . Set MODEL GUARD EXPECT to the model you want to pin; it defaults to claude-fable-5 . Registering on both events matters: UserPromptSubmit catches a downgrade before your next turn spends tokens on the wrong model, and PreToolUse catches one that happens mid-turn, at the first tool call rather than at the end of the turn. When it trips, the session halts with: Model guard: session is running on claude-opus-4-8, expected claude-fable-5. Claude Code reported: "Switched to Opus 4.8 1M context for this session · Fable 5 requires usage credits · /model to change" Run /model to switch back, then resume. Written for anyone rebuilding this from scratch. These are the non-obvious facts; without them the natural design is subtly wrong. 1. No hook event carries the live model. SessionStart is the only event with a model field, it fires once at startup, and it is not guaranteed to be present. There is no $CLAUDE MODEL environment variable and no "model changed" event to subscribe to. A guard built on SessionStart looks correct, passes a casual test, and then silently never fires for the actual problem — it reports the model you started on and never speaks again. 2. Every hook event does receive transcript path on stdin. The transcript is JSONL, and each assistant line carries .message.model naming the model that actually served it. A mid-session switch appears as that value changing from one line to the next. This is the signal to build on. 3. Filter