SLOPSHOPPER

opus-guard

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

newband
v0.1.4MITupdated 2026-10-14acidprime/opus-guard
A shopper browsing a rack in a slop shop
README

opus-guard

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.

What it hooks

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.

Install

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.

Develop

claude plugin validate .
claude plugin test .

License

MIT. See LICENSE. Privacy: PRIVACY.md.

Source 2 files
hooks/register.tsx 58 lines
1import { 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}
58
types/index.d.ts 6 lines
1declare module 'claude-code' {
2  interface PluginState {
3    'opus-guard': { model: string | null; dismissedFor: string | null }
4  }
5}
6