cd /news/developer-tools/claude-code-hook-halt-the-session-wh… · home topics developer-tools article
[ARTICLE · art-66497] src=gist.github.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

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.

read3 min views5 publishedJul 21, 2026

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 <synthetic>. Some assistant entries carry that literal string as the model and will produce false readings. Use

fromjson?

so malformed or partial lines are skipped rather than raising.4. A forced switch writes its own record, one line before the first assistant message on the new model:

{"type":"system","subtype":"model_consent_fallback","level":"warning",
 "content":"Switched to Opus 4.8 (1M context) for this session · Fable 5 requires usage credits · /model to change"}

Trigger on .message.model

rather than this notice — the model field also catches a manual /model

switch and fallback-chain switches, which produce no such notice. Use the notice only to quote why in the stop message.

5. To halt, print {"continue": false, "stopReason": "..."}

to stdout and exit 0. Exit 0 silently on any unreadable, missing, or empty transcript. A false halt that kills a working session is worse than a missed detection, so only stop when a model has been positively read and differs from expectation.

6. Read only the file tail. Transcripts reach 10MB+; a full scan costs ~108ms per event versus ~20ms for a 256KB tail slice. Drop the first line of the slice, which is usually truncated mid-JSON, and fall back to a full scan if the slice yields no model.

7. Detection is inherently one message late. The switch happens server-side when the request is made, so nothing exists for the hook to read until a turn has already run on the new model. You stop at message 1, not message 0. Don't go hunting for an earlier interception point; there isn't one.

For the safety-classifier fallback specifically, Claude Code ships options that are cleaner than any hook, though neither covers a credit-driven switch:

/config

→ turn off "switch models when a message is flagged", so a flagged request s and asks instead of switching silently.- An availableModels

allowlist that excludes the fallback target, which makes the flagged request refuse rather than downgrade.

── more in #developer-tools 4 stories · sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/claude-code-hook-hal…] indexed:0 read:3min 2026-07-21 ·