SLOPSHOPPER

model-router

Runs Explore subagents on haiku and general-purpose ones on sonnet unless a model was asked for

newcommandtoastnetworkagents
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · model-router
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /router ⎿ model-router: 🪙 Jev routing is off. Use /router jev on | jev off. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

claude-code-mods

A few mods for Claude Code, packaged as a plugin marketplace.

ModWhat it does
pomodoroA pixel-art pomodoro timer in a side pane. Day and night skies, animated sun, clouds, stars and meteors, a grass field and a daily tomato tally. Toast and chime when a phase ends.
agent-progressLive Powerline-style progress bars for running subagents, with a moving glint and the model each one runs on.
cmd-guardStops destructive shell commands (rm -rf on /, ~ or *, force pushes, hard resets, dropping tables, mkfs, dd…) and asks what to do: refuse, allow once, trust for this session, or use a safer alternative.
model-routerRuns Explore subagents on haiku and general-purpose subagents on sonnet to save cost. An explicit model on the Agent call always wins; forks and workflows are left alone. Optionally lets Jev pick the model per task.

<img src="docs/pomodoro-focus.png" alt="Pomodoro focus mode: day sky with sun and clouds" width="280"> &nbsp; <img src="docs/pomodoro-break.png" alt="Pomodoro break mode: night sky with moon and stars" width="280">

Install

claude plugin marketplace add redjackfred/claude-code-mods
claude plugin install pomodoro@claude-code-mods
claude plugin install agent-progress@claude-code-mods
claude plugin install cmd-guard@claude-code-mods
claude plugin install model-router@claude-code-mods

Install only the ones you want. Restart Claude Code afterwards.

Usage

pomodoro

/focus [minutes]   start a focus session (default 25)
/focus skip        skip to the next phase
/focus stop        stop the timer
/focus show|hide   show or hide the side pane
  • Runs entirely locally: the timer never calls the model and costs no tokens.
  • Best in a terminal with a Nerd Font and Unicode 13 sextant glyphs (Ghostty, WezTerm, Kitty, iTerm2…).
  • The chime plays through afplay, so sound is macOS only.

agent-progress

agent-progress: three subagents running with Powerline progress bars

/agent-progress toggles the bars on and off.

cmd-guard

cmd-guard asking what to do with a broad rm -rf

Works automatically; /guard off turns it off for the session and /guard on back on. When nobody answers the prompt (or it fails), the command is blocked.

The dialog speaks the language you mostly write in during the session: English, or Traditional Chinese (繁體中文).

It matches commands with regexes, not a shell parser, so treat it as a seatbelt against slips rather than a sandbox: a determined command can still get past it. It also can't tell running a command from mentioning one, so a commit message or heredoc that only quotes a risky command gets stopped too. Reword the text, or /guard off for a moment.

model-router

<img src="docs/model-router.png" alt="model-router toasts: Explore routed to haiku, general-purpose to sonnet" width="360">

Works automatically and shows a toast for each subagent it reroutes. To steer the main agent, you can add this to your ~/.claude/CLAUDE.md:

A model-router mod runs Explore subagents on haiku and general-purpose ones on sonnet
unless the Agent call names a model. For complex reasoning, large refactors, or
hard-to-find bugs, pass `model: "opus"` on the Agent call.
Optional: let Jev pick the model

Jev is a fast "System One" decision model. With it on, each general-purpose subagent's task is classified as haiku, sonnet or opus work before it starts. In a quick test it took about 0.7 s per call and costs well under a cent.

export TYPESAFE_API_KEY=...   # then restart Claude Code
/router jev on                # /router jev off to stop
  • Privacy: it's off by default because turning it on sends each general-purpose task (its description plus the first 4000 characters of the prompt) to TypeSafe's API.
  • Confidence: Jev's pick is used only when its confidence is at least 0.8.
  • Fallback: if Jev is slow (over 1.5 s), fails, or isn't sure, the subagent falls back to sonnet.
  • Explore subagents always stay on haiku.
  • Toast: shows Jev's pick, e.g. general-purpose → haiku · jev 1.00.

Development

Each mod is a Claude Code plugin with TypeScript hooks in hooks/.

claude plugin validate .        # the marketplace
claude plugin validate pomodoro # one plugin
claude plugin test pomodoro     # its tests

License

MIT

Source 1 files
hooks/register.ts 82 lines
1import type { AgentSpawnInput, Register } from 'claude-code'
2
3// subagent type → model; a call that names its own model is left alone
4const ROUTES: Record<string, string> = { Explore: 'haiku', 'general-purpose': 'sonnet' }
5
6export const route = (e: Pick<AgentSpawnInput, 'subagentType' | 'model' | 'fork' | 'workflow'>) =>
7  e.model || e.fork || e.workflow ? undefined : ROUTES[e.subagentType]
8
9// With `/router jev on` and TYPESAFE_API_KEY set, Jev (a System One model) reads a general-purpose
10// task and picks its tier. Its pick wins only when it is confident; a slow,
11// failed or unsure answer keeps the rule's model.
12const JEV_URL = 'https://api.typesafe.ai/v1/systemone'
13const JEV_TIMEOUT_MS = 1500
14export const MIN_CONFIDENCE = 0.8
15// ponytail: the task is cut to its head, so a long prompt is judged by its start
16const MAX_TASK_CHARS = 4000
17
18export const TIERS = {
19  haiku: 'Simple, mechanical work: find or list files, read and summarize a little code, run a command and report, small obvious edits.',
20  sonnet: 'Ordinary engineering: implement a feature, write tests, review code, fix a bug with a clear cause, research across a few files.',
21  opus: 'Hard reasoning: subtle or hard-to-find bugs, large refactors, architecture or design decisions, tricky concurrency, security analysis.',
22} as const
23type Tier = keyof typeof TIERS
24type JevChoice = { choice?: string; confidence?: number }
25
26export const pick = (answer: JevChoice | undefined): { model: Tier; confidence: number } | undefined =>
27  answer?.choice && answer.choice in TIERS && (answer.confidence ?? 0) >= MIN_CONFIDENCE
28    ? { model: answer.choice as Tier, confidence: answer.confidence! }
29    : undefined
30
31export const register: Register = on => {
32  on('session.start', async ($, e, next) => {
33    await $.command.register({ name: 'router', description: 'Let Jev pick general-purpose subagent models (jev on | jev off); sends task text to TypeSafe' })
34    return next(e)
35  })
36
37  // off by default: turning it on sends every general-purpose task to TypeSafe
38  on('command.run', { command: 'router' }, async ($, e) => {
39    const arg = e.args.trim()
40    if (arg !== 'jev on' && arg !== 'jev off') {
41      return { text: `🪙 Jev routing is ${(await $.store.get('jev')) === true ? 'on' : 'off'}. Use /router jev on | jev off.` }
42    }
43    const enable = arg === 'jev on'
44    if (enable && !(await $.env.get('TYPESAFE_API_KEY'))) return { text: '🪙 Set TYPESAFE_API_KEY first, then restart Claude Code.' }
45    await $.store.set('jev', enable)
46    return { text: enable ? '🪙 Jev routing on: general-purpose tasks are sent to TypeSafe to pick a model.' : '🪙 Jev routing off.' }
47  })
48
49  on('agent.spawn', async ($, e, next) => {
50    const rule = route(e)
51    if (!rule) return next(e)
52
53    const useJev = e.subagentType === 'general-purpose' && (await $.store.get('jev')) === true
54    const key = useJev ? await $.env.get('TYPESAFE_API_KEY') : undefined
55    const judged = key ? await ask($, key, e) : undefined
56    const model = judged?.model ?? rule
57    $.ui.toast(`🪙 ${e.subagentType} → ${model}${judged ? ` · jev ${judged.confidence.toFixed(2)}` : ''}`)
58    return next({ ...e, model })
59  })
60}
61
62const ask = async ($: Parameters<Parameters<Register>[0]>[2] extends never ? never : any, key: string, e: AgentSpawnInput) => {
63  const request = $.http.fetch(JEV_URL, {
64    method: 'POST',
65    headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
66    body: JSON.stringify({
67      model: 'jev-latest',
68      state: { description: e.description, task: e.prompt.slice(0, MAX_TASK_CHARS) },
69      questions: {
70        tier: {
71          type: 'choice',
72          instructions: 'Which model tier does a coding agent need to do `task` well? Pick the cheapest tier that will still do it well.',
73          criteria: TIERS,
74        },
75      },
76    }),
77  })
78    .then((r: { ok: boolean; text: string }) => (r.ok ? pick(JSON.parse(r.text).answers?.tier) : undefined))
79    .catch(() => undefined) // network, auth or parse failure: the rule decides
80  return Promise.race([request, $.clock.sleep(JEV_TIMEOUT_MS).then(() => undefined)])
81}
82