SLOPSHOPPER

once

Run one prompt on another model and/or effort: `$opus:max fix the test`. The next prompt is back on your usual settings.

newprompt
v0.1.0MITupdated 2026-10-15DzmitryShut/once
A shopper browsing a rack in a slop shop
README

once

A Claude Code mod that runs a single prompt on another model, another effort level, or both. Your next prompt runs on your usual settings again.

once in a Claude Code session

Install

At the prompt of a Claude Code session:

/plugin install once --marketplace DzmitryShut/once

Answer y to add the marketplace, then pick a scope. From a shell:

claude plugin install once --marketplace DzmitryShut/once

In the desktop app, click + next to the prompt box, then Plugins, Add marketplace with DzmitryShut/once, then Add plugin.

Usage

Start a prompt with $ and a spec:

$opus:max find the race in the order book
$opus     review this diff
$max      why does this test flake?
$haiku:low rename fetchAll to loadAll
SpecModelEffort
$opusOpusunchanged
$maxunchangedmax
$opus:max or $max:opusOpusmax
$claude-sonnet-5-5:lowthat exact model idlow

Models: opus, sonnet, haiku, fable, or any full claude-* id. Effort: low, medium, high, xhigh, max. Case does not matter.

The prefix is removed before the prompt reaches the model. Text that doesn't parse as a spec is sent as typed, so $HOME is wrong and costs $5 are left alone.

How it works

once never changes your session's model or effort. It hooks the model requests of the turn your prompt starts and swaps model and effort on each of them, including every request after a tool call. Once the turn ends, requests go out with the session settings again.

  • Only the main agent is affected. Subagents started during that turn keep their own models.
  • If you type a $ prompt while Claude is still working, it applies from the next model request.
  • If you interrupt a turn before it reaches the model, the override is dropped.

Switching models mid-conversation skips the prompt cache, so the first request on another model costs more and takes longer than usual.

Checked

Tested against Claude Code 2.1.300. Live runs with claude -p --plugin-dir:

  • a session on Sonnet, $haiku with a Bash call in the middle: every request of the turn went to claude-haiku-5-5; the next prompt went back to claude-sonnet-5-5
  • the same math question on Opus: $low thought for 166 tokens and answered in 8 s, $max thought for 322,488 tokens over 41 min

Unit tests:

claude plugin test .

Develop

claude --plugin-dir /path/to/once
claude plugin validate .

The whole mod is hooks/register.ts. Model aliases live in the MODELS table at the top.

License

MIT

Source 1 files
hooks/register.ts 73 lines
1import type { Register } from 'claude-code'
2
3// `$opus fix it`, `$max fix it`, `$opus:max fix it`: the prefix is stripped and
4// the main agent's model requests for that one prompt use the override. The
5// session's own model and effort are never touched, so the next prompt runs
6// on them as before.
7
8const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'] as const
9type Effort = (typeof EFFORTS)[number]
10
11const MODELS: Record<string, string> = {
12  opus: 'claude-opus-5-5',
13  sonnet: 'claude-sonnet-5-5',
14  haiku: 'claude-haiku-5-5',
15  fable: 'claude-fable-5-1',
16}
17
18type Override = { model?: string; effort?: Effort }
19
20const PREFIX = /^\$([\w.:-]+)\s+(\S[\s\S]*)$/
21
22const isEffort = (s: string): s is Effort => (EFFORTS as readonly string[]).includes(s)
23
24function parse(text: string): { override: Override; text: string } | undefined {
25  const [, spec, rest] = PREFIX.exec(text) ?? []
26  if (!spec || !rest) return undefined
27  const override: Override = {}
28  for (const token of spec.toLowerCase().split(':')) {
29    const model = MODELS[token] ?? (/^claude-[\w.-]+$/.test(token) ? token : undefined)
30    if (isEffort(token) && !override.effort) override.effort = token
31    else if (model && !override.model) override.model = model
32    else return undefined
33  }
34  return { override, text: rest }
35}
36
37export const register: Register = on => {
38  // Armed by a prompt, bound to the main loop's turn at its next model request.
39  let pending: { override: Override; during?: string } | undefined
40  let active: { override: Override; turnId: string } | undefined
41
42  on('prompt.submit', async ($, e, next) => {
43    if (e.command || e.isMeta) return next(e)
44    const parsed = parse(e.text)
45    if (!parsed) return next(e)
46    pending = { override: parsed.override, during: e.turnId }
47    const entered = await next({ ...e, text: parsed.text })
48    if (entered.drop !== undefined) pending = undefined
49    return entered
50  }).catch(($, e, next) => next(e))
51
52  on('turn.step', async function* ($, e, next) {
53    if (e.agentId !== undefined) return yield* next(e)
54    if (pending) {
55      active = { override: pending.override, turnId: e.turnId }
56      pending = undefined
57    }
58    if (active?.turnId !== e.turnId) return yield* next(e)
59    const { model, effort } = active.override
60    return yield* next({ ...e, model: model ?? e.model, effort: effort ?? e.effort })
61  })
62
63  on('turn.complete', ($, e, next) => {
64    if (e.agentId === undefined) {
65      if (active?.turnId === e.turnId) active = undefined
66      // A prompt typed over this turn waits for the next one; any other
67      // pending prompt never reached a model request (the turn was aborted).
68      if (pending && pending.during !== e.turnId) pending = undefined
69    }
70    return next(e)
71  })
72}
73