SLOPSHOPPER

effort-per-model

Remembers the effort level for each model and restores it when you switch back

newtoasttimer
v0.2.0MITupdated 2026-10-12malitov/effort-per-model
A shopper browsing a rack in a slop shop
README

effort-per-model

A Claude Code mod that gives every model its own effort level.

Claude Code keeps one effort setting for the whole session. Switch from Sonnet on high to Opus, set medium to plan, switch back to Sonnet to implement, and Sonnet is now on medium too. With this mod, Sonnet comes back on high.

Install

In a terminal session of Claude Code (v2.1.287 or later):

/plugin install effort-per-model --marketplace malitov/effort-per-model

Answer y to add the marketplace, then pick a scope (user scope makes it load in every session, the Desktop app's Code tab included).

How it works

  • Every request to the main model, the mod records the effort it was sent with, under that model's name. The record persists across sessions.
  • The moment the model changes (checked every second), the mod runs /effort <level> with that model's saved effort, so the session and the UI switch right away. A toast says effort high restored for claude-sonnet-5-5.
  • Each request is checked too: if the switch slipped past the poll and you haven't touched the effort, the request is sent with the saved one.
  • Change the effort after switching and that becomes the one saved for the model.
  • Subagents and models without an effort setting are left alone.

Works with /model, the model picker, opusplan and plan mode, the Desktop app's model menu: the mod watches what each request is actually sent with, not how it was chosen.

Limits

  • A model's effort is known only after you've sent at least one request with it; until then there's nothing to restore.
  • An effort picked in the same step as the model (e.g. in the /model picker) is overridden by the saved one; set it with /effort after switching.
  • A new session also starts on the model's saved effort, even if you started it with --effort.

Develop

claude plugin validate .
claude plugin test .
claude --plugin-dir .

hooks/decide.ts holds the decision, hooks/register.ts wires it to the turn.step event.

License

MIT

Source 2 files
hooks/register.ts 53 lines
1import type { Register } from 'claude-code'
2import { decide, type Effort, type Seen } from './decide'
3
4const KEY = 'byModel'
5let seen: Seen = {}
6let polled: string | undefined
7let applied: string | undefined // `${model}:${effort}` last put on the session
8
9async function saved($: any): Promise<Record<string, Effort>> {
10  return ((await $.store.get(KEY)) ?? {}) as Record<string, Effort>
11}
12
13function restore($: any, model: string, effort: Effort) {
14  if (applied === `${model}:${effort}`) return
15  applied = `${model}:${effort}`
16  // queued until the session is idle; makes it the session's effort, so the UI shows it
17  $.command.run({ command: 'effort', args: String(effort) }).catch(() => {})
18  $.ui.toast(`effort ${effort} restored for ${model}`)
19}
20
21export const register: Register = on => {
22  // Switch the effort the moment the model changes, before any prompt is sent.
23  on('session.start', async ($, e, next) => {
24    const r = await next(e)
25    // ponytail: 1s poll, the mod API has no model-changed event
26    $.clock.every(1000, async () => {
27      const model = await $.session.model()
28      if (model === polled) return
29      polled = model
30      const effort = (await saved($))[model]
31      if (effort !== undefined) restore($, model, effort)
32    })
33    return r
34  })
35
36  // Every request: record the effort per model, and restore it if the poll hasn't yet.
37  on('turn.step', async function* ($, e, next) {
38    // subagents run with their own effort; leave them be
39    if (e.agentId !== undefined || e.effort === undefined) return yield* next(e)
40
41    const byModel = await saved($)
42    const d = decide(seen, e.model, e.effort, byModel[e.model])
43    seen = d.seen
44    polled = e.model
45
46    if (d.restore) restore($, e.model, d.use!)
47    applied = `${e.model}:${d.use}`
48    if (byModel[e.model] !== d.use) await $.store.set(KEY, { ...byModel, [e.model]: d.use })
49
50    return yield* next(d.restore ? { ...e, effort: d.use } : e)
51  })
52}
53
hooks/decide.ts 13 lines
1export type Effort = 'low' | 'medium' | 'high' | 'xhigh' | 'max' | number
2export type Seen = { model?: string; effort?: Effort }
3
4// Model changed and the session's effort is still the one carried over
5// (not touched since the last request) → use what this model had last time.
6export function decide(seen: Seen, model: string, effort: Effort | undefined, saved: Effort | undefined) {
7  const carried = seen.model === undefined || effort === seen.effort
8  const restore = effort !== undefined && model !== seen.model && carried && saved !== undefined && saved !== effort
9  const use = restore ? saved : effort
10  // a model without effort leaves the last seen effort as it was
11  return { use, restore, seen: { model, effort: use ?? seen.effort } }
12}
13