{"slug": "claude-code-hook-halt-the-session-when-the-model-is-downgraded-mid-session-e-g-5", "title": "Claude Code hook: halt the session when the model is downgraded mid-session (e.g. Fable 5 -> Opus 4.8)", "summary": "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.", "body_md": "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.\n\nWithout 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.\n\n```\nmkdir -p .claude/hooks\ncurl -o .claude/hooks/model-guard.sh \\\n  https://gist.githubusercontent.com/jimmc414/6e7d61bfe2af477b30b444802f64dbf2/raw/model-guard.sh\nchmod +x .claude/hooks/model-guard.sh\n```\n\nRegister it in `.claude/settings.json`\n\n(or `~/.claude/settings.json`\n\nfor all\nprojects):\n\n```\n{\n  \"hooks\": {\n    \"UserPromptSubmit\": [\n      {\"hooks\": [{\"type\": \"command\", \"command\": \"${CLAUDE_PROJECT_DIR}/.claude/hooks/model-guard.sh\"}]}\n    ],\n    \"PreToolUse\": [\n      {\"hooks\": [{\"type\": \"command\", \"command\": \"${CLAUDE_PROJECT_DIR}/.claude/hooks/model-guard.sh\"}]}\n    ]\n  }\n}\n```\n\nRequires `jq`\n\n. Set `MODEL_GUARD_EXPECT`\n\nto the model you want to pin; it\ndefaults to `claude-fable-5`\n\n.\n\nRegistering on both events matters: `UserPromptSubmit`\n\ncatches a downgrade\nbefore your next turn spends tokens on the wrong model, and `PreToolUse`\n\ncatches one that happens mid-turn, at the first tool call rather than at the\nend of the turn.\n\nWhen it trips, the session halts with:\n\n```\nModel guard: session is running on claude-opus-4-8, expected claude-fable-5.\nClaude Code reported: \"Switched to Opus 4.8 (1M context) for this session ·\nFable 5 requires usage credits · /model to change\" Run /model to switch back,\nthen resume.\n```\n\nWritten for anyone rebuilding this from scratch. These are the non-obvious facts; without them the natural design is subtly wrong.\n\n**1. No hook event carries the live model.** `SessionStart`\n\nis the only event\nwith a `model`\n\nfield, it fires once at startup, and it is not guaranteed to be\npresent. There is no `$CLAUDE_MODEL`\n\nenvironment variable and no \"model\nchanged\" event to subscribe to. A guard built on `SessionStart`\n\nlooks correct,\npasses a casual test, and then silently never fires for the actual problem —\nit reports the model you started on and never speaks again.\n\n**2. Every hook event does receive transcript_path on stdin.** The transcript\nis JSONL, and each assistant line carries\n\n`.message.model`\n\nnaming the model\nthat actually served it. A mid-session switch appears as that value changing\nfrom one line to the next. This is the signal to build on.**3. Filter <synthetic>.** Some assistant entries carry that literal string\nas the model and will produce false readings. Use\n\n`fromjson?`\n\nso malformed or\npartial lines are skipped rather than raising.**4. A forced switch writes its own record**, one line *before* the first\nassistant message on the new model:\n\n```\n{\"type\":\"system\",\"subtype\":\"model_consent_fallback\",\"level\":\"warning\",\n \"content\":\"Switched to Opus 4.8 (1M context) for this session · Fable 5 requires usage credits · /model to change\"}\n```\n\nTrigger on `.message.model`\n\nrather than this notice — the model field also\ncatches a manual `/model`\n\nswitch and fallback-chain switches, which produce no\nsuch notice. Use the notice only to quote *why* in the stop message.\n\n**5. To halt**, print `{\"continue\": false, \"stopReason\": \"...\"}`\n\nto stdout and\nexit 0. Exit 0 silently on any unreadable, missing, or empty transcript. A\nfalse halt that kills a working session is worse than a missed detection, so\nonly stop when a model has been positively read and differs from expectation.\n\n**6. Read only the file tail.** Transcripts reach 10MB+; a full scan costs\n~108ms per event versus ~20ms for a 256KB tail slice. Drop the first line of\nthe slice, which is usually truncated mid-JSON, and fall back to a full scan\nif the slice yields no model.\n\n**7. Detection is inherently one message late.** The switch happens server-side\nwhen the request is made, so nothing exists for the hook to read until a turn\nhas already run on the new model. You stop at message 1, not message 0. Don't\ngo hunting for an earlier interception point; there isn't one.\n\nFor the safety-classifier fallback specifically, Claude Code ships options that are cleaner than any hook, though neither covers a credit-driven switch:\n\n`/config`\n\n→ turn off \"switch models when a message is flagged\", so a flagged request pauses and asks instead of switching silently.- An\n`availableModels`\n\nallowlist that excludes the fallback target, which makes the flagged request refuse rather than downgrade.", "url": "https://wpnews.pro/news/claude-code-hook-halt-the-session-when-the-model-is-downgraded-mid-session-e-g-5", "canonical_source": "https://gist.github.com/jimmc414/6e7d61bfe2af477b30b444802f64dbf2", "published_at": "2026-07-21 05:39:46+00:00", "updated_at": "2026-07-21 05:57:21.735011+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models", "ai-products"], "entities": ["Claude Code", "Fable 5", "Opus 4.8"], "alternates": {"html": "https://wpnews.pro/news/claude-code-hook-halt-the-session-when-the-model-is-downgraded-mid-session-e-g-5", "markdown": "https://wpnews.pro/news/claude-code-hook-halt-the-session-when-the-model-is-downgraded-mid-session-e-g-5.md", "text": "https://wpnews.pro/news/claude-code-hook-halt-the-session-when-the-model-is-downgraded-mid-session-e-g-5.txt", "jsonld": "https://wpnews.pro/news/claude-code-hook-halt-the-session-when-the-model-is-downgraded-mid-session-e-g-5.jsonld"}}