Shift Claude Code model and reasoning effort in one command: /m opus high

Shift Claude Code's model and reasoning effort in one command.
/m haiku low # quick question, cheap and fast
/m opus high # hard refactor
/m sonnet # model only
/m xhigh # effort only
Without cc-model-effort-switcher it takes two commands, /model opus and then /effort high, or a trip through the model picker. /m does both at once.
At the Claude Code prompt:
/plugin install cc-model-effort-switcher --marketplace kuzmany/cc-model-effort-switcher
Or from a shell:
claude plugin marketplace add kuzmany/cc-model-effort-switcher
claude plugin install cc-model-effort-switcher@cc-model-effort-switcher
/m <model> <effort>, in either order, or just one of them.
| Model | Effort |
|---|---|
haiku, sonnet, opus, fable, default, or a full claude-* id | low, medium, high, xhigh, max |
Anything else prints a usage line and changes nothing.
/m runs the built-in /model and /effort for you, in that order, right after it finishes. The result is exactly what typing them would do. That also means /m opus … saves Opus as your default for new sessions, as /model opus does.
cc-model-effort-switcher changes no files or settings of its own and makes no network calls.
Claude Code with mods (function hooks), 2.1.287 or later. Tested with 2.1.299.
claude plugin validate .
claude plugin test .
claude --plugin-dir .
MIT
hooks/register.ts 61 lines1import type { Register } from 'claude-code'
2
3const MODELS = ['haiku', 'sonnet', 'opus', 'fable', 'default'] as const
4const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'] as const
5const FULL_MODEL_ID = /^claude-[a-z0-9.-]+(\[1m\])?$/
6const USAGE = 'Usage: /m <model> <effort>, /m <model>, or /m <effort>. Models: haiku, sonnet, opus, fable, default, or a claude-* id. Effort: low, medium, high, xhigh, max.'
7
8type Plan = { model?: string; effort?: string } | { error: string }
9
10export function plan(args: string): Plan {
11 const words = args.trim().toLowerCase().split(/\s+/).filter(Boolean)
12 if (words.length === 0 || words.length > 2) {
13 return { error: USAGE }
14 }
15
16 let model: string | undefined
17 let effort: string | undefined
18 for (const word of words) {
19 if ((EFFORTS as readonly string[]).includes(word) && effort === undefined) {
20 effort = word
21 } else if (((MODELS as readonly string[]).includes(word) || FULL_MODEL_ID.test(word)) && model === undefined) {
22 model = word
23 } else {
24 return { error: `Unknown value "${word}". ${USAGE}` }
25 }
26 }
27
28 return { model, effort }
29}
30
31export const register: Register = on => {
32 on('session.start', async ($, e, next) => {
33 await $.command.register({ name: 'm', description: 'Switch model and effort at once: /m haiku low' })
34
35 return next(e)
36 })
37
38 on('command.run', { command: 'm' }, async ($, e) => {
39 const chosen = plan(e.args)
40 if ('error' in chosen) {
41 return { text: chosen.error }
42 }
43
44 // A command.run hook cannot run another command itself; a timer runs them once this one has finished.
45 $.clock.after(0, async () => {
46 try {
47 if (chosen.model !== undefined) {
48 await $.command.run({ command: 'model', args: chosen.model })
49 }
50 if (chosen.effort !== undefined) {
51 await $.command.run({ command: 'effort', args: chosen.effort })
52 }
53 } catch (err) {
54 await $.ui.toast(`/m failed: ${String(err)}`)
55 }
56 })
57
58 return { text: `Switching to ${[chosen.model, chosen.effort].filter(Boolean).join(' · ')}` }
59 })
60}
61