A Tools button at the right end of the prompt footer that opens a small box to switch the model, the effort and your daymods on or off.

<a href="LICENSE"><img alt="MIT license" src="https://img.shields.io/badge/license-MIT-34d399?style=flat-square"></a> <img alt="Claude Code 2.1.300+" src="https://img.shields.io/badge/Claude%20Code-2.1.300%2B-818cf8?style=flat-square"> <img alt="mods" src="https://img.shields.io/badge/mods-4-f472b6?style=flat-square">
A collection of small mods for Claude Code. Each mod is its own plugin, so you install only the ones you want.
Mods use Claude Code's hooks-module API, which is early access and may change between releases. They were built and tested on Claude Code 2.1.300.
| Mod | What it does |
|---|---|
| autoroute | Picks the model and effort for each prompt, sends subagents to cheaper models, and keeps Fable and extra-usage spend in check. |
| prompt-upgrade | Catches vague prompts, asks one quick multiple-choice question, rewrites the prompt with a working method, and lets you review it before it runs. |
| box-tools | A filled ✦ Tools button at the right end of the prompt footer. Click it for a small box where you switch the model, the effort, and each of your daymods on or off. |
| pace-statusline | A 4-line status line with limit bars, reset times and a pace prediction (⚠ runs out today 4:19pm) so you know before you hit the wall. Lives in its own repo. |
More mods will be added here as they are ready.
Add the marketplace once, then install any mod from it.
In a Claude Code session:
/plugin install autoroute --marketplace daydeda/daymods
Or from a shell:
claude plugin marketplace add daydeda/daymods
claude plugin install autoroute@daymods
claude plugin install prompt-upgrade@daymods
claude plugin install box-tools@daymods
claude plugin install pace-statusline@daymods
Answer y when asked to add the marketplace and pick a scope. The mod is active right away. Mods installed at the user scope load in every later session.
To remove one: claude plugin uninstall autoroute.
Routes each prompt to a suitable model and effort, so small jobs stop burning Opus tokens and big jobs get a plan.
| Tier | Typical prompts | Model | Effort |
|---|---|---|---|
| trivial | a question, typo, rename, quick lookup | Haiku | low |
| normal | a feature or fix in 1-3 files | Sonnet | medium |
| hard | tricky debugging, design, refactors | Opus | high |
| large | migrations, rewrites, architecture | Opus | high, plus a delegation plan |
Short follow-ups like "continue" or "yes" keep the previous route.
flowchart LR
A([your prompt]) --> B{rules}
B -->|clear| D[tier]
B -->|unclear| C[Haiku classifier] --> D
D --> E{plan and spend gate}
E -->|Fable, not Max| F[Opus]
E -->|limit near, extra usage on| G[Sonnet cap]
E -->|ok| H[chosen model]
These apply to every request, whatever chose its model, and stay on even when routing is turned off.
~/.claude.json). Anything other than a Max plan, or a plan it cannot read, is downgraded from Fable to Opus. This covers the router, !fable, Fable picked in /model, and Fable subagents.The plan check reads a local file the user can edit. It prevents accidental charges; it is not a security boundary.
~/.claude/autoroute.state, and a status line script can show it inline. pace-statusline does: Opus ▶ high ⇢ Haiku·low ▶ ⌂ daymods, plus a ⚠ note for downgrades or spend warnings. The ⇢ part is hidden once the running model matches the route. Set display to line to get autoroute's own row instead, for example autoroute: Haiku · low · "typo" (rules).STATUSLINE_COMPACT=1 folds ctx, 5-hour, weekly and session stats into one row with short bars. The reset countdown shows only past 50% used.!haiku, !sonnet, !opus, !fable or !max. !auto routes normally./route on|off|status: turn routing on or off, or show the last route, your plan, and the spend guard state./config):classifier: rules+haiku (default) or rules only. Rules-only makes no model call.display: statusline (default) writes the route for your status line to show; line shows autoroute's own row.spendGuardPercent: threshold for the spend guard, default 90.! overrides, or /route status to see why a route was chosen.mods/autoroute/hooks/route.ts. If your Claude Code build uses different IDs, change them there.Turns a vague prompt like "make a todo app" into a clear one before Claude starts, and lets you check it first.
The panel opens beside the transcript (or above the prompt). A step bar at the top shows where you are: ✓ Your prompt → ● Questions 2/3 → ○ Rewrite → ○ Review. Keys:
1-3 pick a choice, the next number types your own answer, s skips the question, x skips the rest and rewrites now, b goes back to the previous question (your earlier answer is marked ✓).1-4 as above, 5 asks for a change, b goes back to the questions with your answers kept, then rewrites again.Esc sends your prompt as typed. Back, Skip the rest and Esc sit together in a bar at the bottom of the panel.Each question is its own card (QUESTION 2 OF 3, one dot per question). The panel uses Claude's orange for what is current and green for what is done, and opens at about 60% of the terminal width once you have run /refine once (100 columns before that).
Typing your own answer. Claude Code's text field is one line and cuts long text off, so the panel draws its own wrapped text box instead. Press the typing key (or click the box), type, and press Enter. It supports typing, Backspace and Ctrl+U to clear; there is no cursor movement. It is not available in the VS Code and mobile layouts, which only get the numbered choices and Skip.
The panel needs a terminal about 110 columns wide; on a narrower one it falls back to Claude Code's question dialog, with the rewrite shown inside the question. Set ui to dialog to always use the dialog.
Each rewrite picks one working method and writes its step into the prompt:
| Method | Idea | What the prompt asks for |
|---|---|---|
| Direct | prompt engineering | A clear spec and nothing else, for small well-defined changes. |
| Context first | context engineering | Read specific code, docs or data before deciding. |
| Plan first | plan, then build | A short plan and a pause for your OK before editing, for big or open-ended work. |
| Verify loop | loop engineering | A named check (tests, build, lint) and iterating until it passes, stopping after 3 failed tries. |
| Make it a rule | harness engineering | Do the task now, then suggest making it permanent as a CLAUDE.md rule, hook, skill or /loop. |
/refine <prompt>: upgrade one prompt on demand, whatever the mode./refine auto|always|off: when it steps in. auto (default) handles vague prompts only, always every new task you type, off only /refine <prompt>./refine status: the mode, and why the last prompt was or wasn't upgraded./config): mode, detector (rules+haiku or rules alone), ui (panel or dialog), and rewriteModel (sonnet by default, or haiku or opus).!opus-style autoroute override is kept in front of the rewritten prompt.A filled orange ✦ Tools pill sits at the right end of the prompt footer. Click it and a small box opens beside the transcript:
✦ Tools Opus · high
╭─────────────────────────────────────────────────────────╮
│ ◆ Model 1-5 │
│ 1: ○ Default 2: ○ Haiku 3: ○ Sonnet 4: ● Opus 5: ○ Fable │
╰─────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────╮
│ ⚡ Effort q w e r t │
│ q: ○ low w: ○ medium e: ● high r: ○ xhigh t: ○ max │
╰─────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────╮
│ ❖ Mods a s d … │
│ a: ● on autoroute │
╰─────────────────────────────────────────────────────────╯
✦ Tools, or run /tools. Clicking the button again closes the box.1-5 for models, q w e r t for effort, a s d… for mods.x, or the button again.model row of /config, the same as picking it there. It becomes your default for new sessions too./effort <level>, as if you typed it. Claude Code saves the level for the current model, so it carries over to new sessions.claude plugin enable or claude plugin disable, then /reload-plugins, so the change applies right away and stays on or off in later sessions.--plugin-dir aren't installed, so they don't appear in the list./config): button (show or hide). With hide, the footer is left alone and /tools opens the box.Each mod lives in mods/<name>/ with the standard layout:
mods/<name>/
├── .claude-plugin/plugin.json
├── hooks/hooks.json
├── hooks/register.ts
└── tests/
Check and test a mod with:
claude plugin validate mods/autoroute
claude plugin test mods/autoroute
Try one without installing it: claude --plugin-dir mods/autoroute.
hooks/register.tsx 280 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, RenderChildren, Timer } from 'claude-code'
3
4import { EFFORTS, EFFORT_KEYS, MODEL_KEYS, MOD_KEYS, firstLine, modelChoices, parseMods, title } from './tools'
5import type { View } from '../types'
6
7const PANE = 'box-tools'
8const FOCUS_POLL_MS = 250
9
10let showButton = true
11
12const EMPTY: View = { model: '', models: [], effort: '', mods: [], note: '', busy: '' }
13const view = atom({ plugin: 'box-tools', key: 'view' } as const, EMPTY)
14
15// The model as /config holds it, and the plain choices it offers.
16async function readModel($: EngineInterface): Promise<Pick<View, 'model' | 'models'>> {
17 try {
18 const row = (await $.config.list()).find(r => r.key === 'model')
19 if (!row || row.isLocked) return { model: '', models: [] }
20 return { model: String(row.value), models: modelChoices(row.options) }
21 } catch {
22 return { model: '', models: [] }
23 }
24}
25
26async function readMods($: EngineInterface) {
27 try {
28 const { exitCode, stdout } = await $.process.run(['claude', 'plugin', 'list', '--json'])
29 return exitCode === 0 ? parseMods(stdout) : undefined
30 } catch {
31 return undefined
32 }
33}
34
35// /effort saves the level per model (`modelSettings`), over the top-level default.
36async function readEffort($: EngineInterface): Promise<string> {
37 try {
38 const [settings, modelId] = await Promise.all([$.settings.read(), $.session.model()])
39 const perModel = (settings as { modelSettings?: Record<string, { effortLevel?: unknown }> }).modelSettings
40 const level = perModel?.[modelId]?.effortLevel ?? settings.effortLevel
41 return typeof level === 'string' ? level : ''
42 } catch {
43 return ''
44 }
45}
46
47async function refresh($: EngineInterface) {
48 const [model, mods, effort] = await Promise.all([readModel($), readMods($), readEffort($)])
49 await update($, view, (v): View => ({
50 ...v,
51 ...model,
52 effort: effort || v.effort,
53 mods: mods ?? v.mods,
54 note: mods === undefined ? "Couldn't read your installed plugins." : v.note,
55 }))
56}
57
58// Effort colors follow the theme, cool to hot, like the status line's.
59const EFFORT_COLORS: Record<string, string> = { low: 'success', medium: 'suggestion', high: 'warning', xhigh: 'error', max: 'error' }
60
61const PANE_ARGS = { id: PANE, title: 'Tools', focus: true, closeOnEscape: true, columns: 74, rows: 24 } as const
62
63// Set while a change runs, and for a moment after: the command it runs takes the keyboard,
64// sometimes just after it has finished, and that is not a click away.
65let isChanging = false
66let graceUntil = 0
67const GRACE_MS = 1000
68
69// One change at a time: a press while another runs is ignored.
70async function run($: EngineInterface, busy: string, work: () => Promise<Partial<View>>) {
71 if (isChanging || (await read($, view)).busy) return
72 isChanging = true
73 try {
74 await update($, view, (v): View => ({ ...v, busy, note: '' }))
75 const changed = await work().catch((err: unknown): Partial<View> => ({ note: `Failed: ${firstLine(String(err))}` }))
76 await update($, view, (v): View => ({ ...v, ...changed, busy: '' }))
77 } finally {
78 isChanging = false
79 graceUntil = (await $.clock.now()) + GRACE_MS
80 }
81 await takeKeyboard($)
82}
83
84// A command run from the box (/effort, /reload-plugins) hands the keyboard to the prompt, and an
85// open pane can't ask for it again: open the box afresh, which takes it over the empty prompt.
86async function takeKeyboard($: EngineInterface) {
87 const pane = (await $.ui.panes()).find(p => p.id === PANE)
88 if (!pane || pane.isFocused) return
89 await $.ui.close({ id: PANE }).catch(() => undefined)
90 const opened = await $.ui.open(PANE_ARGS).catch(() => undefined)
91 if (opened?.isPlaced) watchFocus($)
92}
93
94const setModel = async ($: EngineInterface, model: string) => {
95 if ((await read($, view)).model === model) return void (await update($, view, (v): View => ({ ...v, note: `Already on ${title(model)}.` })))
96 return run($, `Switching to ${title(model)}…`, async () => {
97 const r = await $.config.set({ key: 'model', value: model })
98 if ('deny' in r && r.deny) return { note: `Model not changed: ${r.deny}` }
99 // Effort is kept per model: show the new model's.
100 const effort = await readEffort($)
101 return { model, ...(effort ? { effort } : {}), note: `Model set to ${title(model)}.` }
102 })
103}
104
105const setEffort = async ($: EngineInterface, effort: string) => {
106 if ((await read($, view)).effort === effort) return void (await update($, view, (v): View => ({ ...v, note: `Effort is already ${effort}.` })))
107 return run($, `Setting effort to ${effort}…`, async () => {
108 // The built-in /effort, as if typed: it waits until Claude is idle.
109 const r = await $.command.run({ command: 'effort', args: effort })
110 const said = firstLine('text' in r ? r.text : undefined).split(':')[0] ?? ''
111 return { effort, note: said ? `${said}.` : `Effort set to ${effort}.` }
112 })
113}
114
115const toggleMod = ($: EngineInterface, id: string, name: string, on: boolean) =>
116 run($, `Turning ${name} ${on ? 'on' : 'off'}…`, async () => {
117 const r = await $.process.run(['claude', 'plugin', on ? 'enable' : 'disable', id])
118 if (r.exitCode !== 0) return { note: `${name} not changed: ${firstLine(r.stderr || r.stdout) || `exit ${r.exitCode}`}` }
119 await update($, view, (cur): View => ({
120 ...cur,
121 mods: cur.mods.map(m => (m.id === id ? { ...m, isEnabled: on } : m)),
122 note: `${name} ${on ? 'on' : 'off'}. Reloading plugins…`,
123 busy: '',
124 }))
125 // Reloads this mod too: the box stays open and its state is kept.
126 void $.command.run({ command: 'reload-plugins' }).catch(() => undefined)
127 return {}
128 })
129
130// Click-away: once the box has had the keyboard, losing it (a click in the transcript or the prompt) closes it.
131let watch: Timer | undefined
132let hadFocus = false
133
134function stopWatch() {
135 watch?.cancel()
136 watch = undefined
137}
138
139function watchFocus($: EngineInterface) {
140 stopWatch()
141 hadFocus = false
142 watch = $.clock.every(FOCUS_POLL_MS, () => {
143 void (async () => {
144 if (isChanging) return
145 const pane = (await $.ui.panes()).find(p => p.id === PANE)
146 if (!pane) return stopWatch()
147 if (pane.isFocused) hadFocus = true
148 else if ((await $.clock.now()) < graceUntil) await takeKeyboard($)
149 else if (hadFocus) {
150 stopWatch()
151 await $.ui.close({ id: PANE }).catch(() => undefined)
152 }
153 })()
154 })
155}
156
157async function isOpen($: EngineInterface) {
158 return (await $.ui.panes()).some(p => p.id === PANE)
159}
160
161// The footer button and /tools both toggle the box.
162async function toggle($: EngineInterface): Promise<boolean> {
163 if (await isOpen($)) {
164 stopWatch()
165 await $.ui.close({ id: PANE }).catch(() => undefined)
166 return false
167 }
168 await update($, view, (v): View => ({ ...v, note: '' }))
169 void refresh($)
170 const opened = await $.ui.open(PANE_ARGS)
171 if (opened.isPlaced) watchFocus($)
172 return opened.isPlaced
173}
174
175export const register: Register = (on, options) => {
176 showButton = options.button !== 'hide'
177
178 on('session.start', async ($, e, next) => {
179 await $.command.register({ name: 'tools', description: 'box-tools: open the Tools box (model, effort, mods)' })
180 // A reload with the box open (after a mod toggle) picks the fresh plugin list up
181 // and takes the keyboard back once /reload-plugins has finished with it.
182 if (await isOpen($)) {
183 void refresh($)
184 graceUntil = (await $.clock.now()) + GRACE_MS
185 watchFocus($)
186 $.clock.after(300, () => void takeKeyboard($))
187 }
188 return next(e)
189 })
190
191 on('command.run', { command: 'tools' }, async $ => {
192 const isShown = await toggle($)
193 return { text: isShown ? 'Tools box open. Esc or a click outside closes it.' : 'Tools box closed.' }
194 })
195
196 on('ui.close', { id: PANE }, async ($, e, next) => {
197 stopWatch()
198 return next(e)
199 })
200
201 // The right end of the prompt footer: the engine's mode labels, then the Tools button.
202 on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
203 if (!showButton) return next(e)
204 const { Box, Text, Button } = $.ui.resolve(e)
205 const modes = e.props.modes
206 return (
207 <Box flexDirection="row" gap={1}>
208 {modes.length > 0 && <Text dimColor>{modes.join(' & ')}</Text>}
209 <Button key="tools" plain onPress={() => void toggle($)}>
210 <Text bold inverse color="claude">{' ✦ Tools '}</Text>
211 </Button>
212 </Box>
213 )
214 })
215
216 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
217 const { Box, Text, Button } = $.ui.resolve(e)
218 const v = await read($, view)
219 const autoroute = v.mods.find(m => m.name === 'autoroute')
220 const isFailure = /^(Failed|Couldn)|not changed|not available/i.test(v.note)
221
222 // A chip is one choice: the current one is a filled pill in the section's color, the rest sit dim.
223 const chip = (key: string, hotkey: string | undefined, label: string, isCurrent: boolean, color: string, onPress: () => void) => (
224 <Button key={key} hotkey={hotkey} plain dimColor={!isCurrent} onPress={onPress}>
225 {isCurrent
226 ? <Text bold inverse color={color}>{` ● ${label} `}</Text>
227 : <Text>{` ○ ${label} `}</Text>}
228 </Button>
229 )
230
231 // A titled card: a rounded border in the section's color, the title on top.
232 const card = (icon: string, name: string, color: string, hint: string, children: RenderChildren) => (
233 <Box flexDirection="column" borderStyle="round" borderColor={color} paddingX={1}>
234 <Box flexDirection="row" justifyContent="space-between">
235 <Text bold color={color}>{`${icon} ${name}`}</Text>
236 <Text dimColor>{hint}</Text>
237 </Box>
238 {children}
239 </Box>
240 )
241
242 const modelName = v.model ? title(v.model) : '…'
243 return (
244 <Box flexDirection="column" gap={1}>
245 <Box flexDirection="row" justifyContent="space-between">
246 <Text bold color="claude">{'✦ Tools'}</Text>
247 <Text dimColor>{`${modelName} · ${v.effort || '…'}`}</Text>
248 </Box>
249 {card('◆', 'Model', 'claude', '1-5', (
250 <Box flexDirection="row" flexWrap="wrap">
251 {v.models.length === 0 && <Text dimColor>Not available here (use /model).</Text>}
252 {v.models.map((m, i) => chip(`model-${m}`, MODEL_KEYS[i], title(m), m === v.model, 'claude', () => void setModel($, m)))}
253 </Box>
254 ))}
255 {card('⚡', 'Effort', 'warning', 'q w e r t', (
256 <Box flexDirection="row" flexWrap="wrap">
257 {EFFORTS.map((level, i) => chip(`effort-${level}`, EFFORT_KEYS[i], level, level === v.effort, EFFORT_COLORS[level] ?? 'warning', () => void setEffort($, level)))}
258 </Box>
259 ))}
260 {card('❖', 'Mods', 'success', 'a s d …', (
261 <Box flexDirection="column">
262 {v.mods.length === 0 && <Text dimColor>No other daymods installed.</Text>}
263 {v.mods.map((m, i) => (
264 <Button key={`mod-${m.name}`} hotkey={MOD_KEYS[i]} plain dimColor={!m.isEnabled} onPress={() => void toggleMod($, m.id, m.name, !m.isEnabled)}>
265 <Text bold={m.isEnabled} color={m.isEnabled ? 'success' : undefined}>{`${m.isEnabled ? '● on ' : '○ off'} ${m.name}`}</Text>
266 </Button>
267 ))}
268 </Box>
269 ))}
270 {autoroute?.isEnabled && <Text color="warning" dimColor>{'⚠ autoroute is on: it picks the model and effort per prompt, over these.'}</Text>}
271 {(v.busy || v.note) && <Text color={v.busy ? 'suggestion' : isFailure ? 'error' : 'success'}>{v.busy ? `⏳ ${v.busy}` : `✓ ${v.note}`}</Text>}
272 <Box flexDirection="row" columnGap={2}>
273 <Button key="close" hotkey="x" plain dimColor onPress={() => void $.ui.close({ id: PANE }).catch(() => undefined)}>Close</Button>
274 <Text dimColor>Esc or click away to close</Text>
275 </Box>
276 </Box>
277 )
278 })
279}
280hooks/tools.ts 52 lines1// Pure helpers: no `$`, so they can be read and tested on their own.
2
3import type { Mod } from '../types'
4
5export const SELF = 'box-tools'
6export const MARKETPLACE = 'daymods'
7
8// The plain model choices, in the order the box shows them; `/config` may offer more.
9const MODEL_CHOICES = ['default', 'haiku', 'sonnet', 'opus', 'fable'] as const
10export const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'] as const
11
12// Hotkeys inside the box: digits for models, the qwert row for effort, asdfg… for mods.
13export const MODEL_KEYS = ['1', '2', '3', '4', '5']
14export const EFFORT_KEYS = ['q', 'w', 'e', 'r', 't']
15export const MOD_KEYS = ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l']
16
17export function modelChoices(options: readonly string[] | undefined): string[] {
18 if (!options) return []
19 return MODEL_CHOICES.filter(m => options.includes(m))
20}
21
22export const title = (s: string) => (s ? s[0]!.toUpperCase() + s.slice(1) : s)
23
24/**
25 * The installed daymods, from `claude plugin list --json`. Box Tools itself is left out:
26 * turning it off from its own box would take the box away.
27 */
28export function parseMods(json: string): Mod[] | undefined {
29 let list: unknown
30 try {
31 list = JSON.parse(json)
32 } catch {
33 return undefined
34 }
35 if (!Array.isArray(list)) return undefined
36 const mods: Mod[] = []
37 for (const p of list) {
38 if (!p || typeof p !== 'object') continue
39 const { id, enabled } = p as { id?: unknown; enabled?: unknown }
40 if (typeof id !== 'string') continue
41 const [name, marketplace] = id.split('@')
42 if (marketplace !== MARKETPLACE || !name || name === SELF) continue
43 mods.push({ id, name, isEnabled: enabled === true })
44 }
45 return mods.sort((a, b) => a.name.localeCompare(b.name))
46}
47
48// The first line of a command's output, for the note under the controls.
49export function firstLine(text: string | undefined): string {
50 return (text ?? '').split('\n').map(l => l.trim()).find(Boolean) ?? ''
51}
52types/index.d.ts 20 lines1export type Mod = { id: string; name: string; isEnabled: boolean }
2
3// What the Tools box shows.
4export type View = {
5 model: string
6 models: string[]
7 effort: string
8 mods: Mod[]
9 // One line under the controls: what the last change did, or why it didn't.
10 note: string
11 // Set while a change is running, so a second press waits.
12 busy: string
13}
14
15declare module 'claude-code' {
16 interface PluginState {
17 'box-tools': { view: View }
18 }
19}
20