Warning band above the prompt whenever the session is not on an Opus model

Born when Claude felt dumb and it turned out to be running Fable 5.1 by accident.
A Claude Code mod that puts a warning band above the prompt whenever the session isn't on an Opus model:
⚠ Not on Opus: this session is running claude-fable-5-1. Switch with /model opus [Dismiss]
It checks the live model on every redraw, after /model switches, and after each turn, so a fallback mid-session shows up too. Dismiss hides it until the model changes again.
Every hook only reads; none changes what passes through it.
session.start, turn.complete: read the session's model.classic.PostModelSwitch: reads the model a /model switch landed on, to redraw the band. It doesn't change the switch or settings.ui.render (AbovePrompt): draws the warning when the model isn't Opus, and passes otherwise. It steps aside while a survey holds the band.It makes no network calls and writes no files; the one value it keeps is which model you dismissed the warning for.
At the prompt of a terminal session:
/plugin install opus-guard --marketplace acidprime/opus-guard
Answer y to add the marketplace, then pick a scope (user is the usual one).
From a shell:
claude plugin install opus-guard --marketplace acidprime/opus-guard
In the desktop app: click + next to the prompt box, then Plugins, Add marketplace with acidprime/opus-guard, then Add plugin.
claude plugin validate .
claude plugin test .
MIT. See LICENSE. Privacy: PRIVACY.md.
hooks/register.tsx 58 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4const model = atom({ plugin: 'opus-guard', key: 'model' } as const, null)
5const dismissedFor = atom({ plugin: 'opus-guard', key: 'dismissedFor' } as const, null)
6
7export const isOpus = (m: string) => /opus/i.test(m)
8
9export const register: Register = on => {
10 on('session.start', async ($, e, next) => {
11 const current = await $.session.model()
12 await update($, model, () => current)
13 return next(e)
14 })
15
16 on('classic.PostModelSwitch', async ($, e, next) => {
17 await update($, model, () => e.to_model)
18 return next(e)
19 })
20
21 // Catches changes no switch event reports, like a fallback mid-turn.
22 on('turn.complete', async ($, e, next) => {
23 const current = await $.session.model()
24 if (current !== (await read($, model))) {
25 await update($, model, () => current)
26 }
27 return next(e)
28 })
29
30 // Reads the live model on every draw: the atom writes above only trigger
31 // redraws, and PostModelSwitch never fires where an organization manages
32 // Claude Code.
33 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
34 const current = (await $.session.model()) || (await read($, model))
35 if (e.props.hasSurvey || current === null || isOpus(current)) {
36 return next(e)
37 }
38 if ((await read($, dismissedFor)) === current) {
39 return next(e)
40 }
41
42 const { Box, Button, Text } = $.ui.resolve(e)
43
44 return (
45 <Box>
46 <Text color="warning" bold>
47 ⚠ Not on Opus: this session is running {current}. Switch with /model opus{' '}
48 </Text>
49 <Button
50 key="dismiss"
51 label="Dismiss"
52 onPress={() => update($, dismissedFor, () => current)}
53 />
54 </Box>
55 )
56 })
57}
58types/index.d.ts 6 lines1declare module 'claude-code' {
2 interface PluginState {
3 'opus-guard': { model: string | null; dismissedFor: string | null }
4 }
5}
6