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

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.

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.
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
| Spec | Model | Effort |
|---|---|---|
$opus | Opus | unchanged |
$max | unchanged | max |
$opus:max or $max:opus | Opus | max |
$claude-sonnet-5-5:low | that exact model id | low |
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.
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.
$ prompt while Claude is still working, it applies from the next model request.Switching models mid-conversation skips the prompt cache, so the first request on another model costs more and takes longer than usual.
Tested against Claude Code 2.1.300. Live runs with claude -p --plugin-dir:
$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$low thought for 166 tokens and answered in 8 s, $max thought for 322,488 tokens over 41 minUnit tests:
claude plugin test .
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.
hooks/register.ts 73 lines1import 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