SLOPSHOPPER

usage-bar

A band above the prompt showing your 5-hour and weekly rate-limit usage, with warnings as you approach them.

newbandcommandtoast
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-bar
› 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 › /usage-bar ⎿ usage-bar: Usage band hidden. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

cc-mods

Four Claude Code mods (function-hook plugins) that save tokens and keep you aware of your limits.

ModWhat it does
smart-model-routerPicks a heavy or light model for each turn
usage-barShows 5-hour and weekly rate-limit usage above the prompt
context-barShows how full the context window is, by category, above the prompt
research-offloaderKeeps research work out of the main context

Why: kaizen for your AI bill

Kaizen is continuous improvement through small changes, each removing one source of waste (muda). These mods apply it to a Claude Code workflow:

WasteMod that removes it
The most expensive model doing routine worksmart-model-router
Raw web pages filling the main contextresearch-offloader
Hitting a rate limit by surpriseusage-bar (and the router's usage guard)
Context filling up unnoticedcontext-bar

None of these is a big idea; each is one small fix. Run the cycle on your own usage:

  1. Plan: note your 5-hour and weekly usage over a normal week, using usage-bar.
  2. Do: install the mods and work as usual.
  3. Check: compare the next week's usage against the first.
  4. Adjust: tune usageGuard, the model options and the research trigger phrases, then repeat.

What you can see, you can improve.

Install

Try from a local folder (development)

claude --plugin-dir D:\ai\cc-mods\smart-model-router --plugin-dir D:\ai\cc-mods\usage-bar --plugin-dir D:\ai\cc-mods\context-bar --plugin-dir D:\ai\cc-mods\research-offloader

Saving a file reloads the mod in a running session.

Install from GitHub (timothylok/ClaudeCodeModsByTimLok)

/plugin install smart-model-router --marketplace timothylok/ClaudeCodeModsByTimLok
/plugin install usage-bar --marketplace timothylok/ClaudeCodeModsByTimLok
/plugin install context-bar --marketplace timothylok/ClaudeCodeModsByTimLok
/plugin install research-offloader --marketplace timothylok/ClaudeCodeModsByTimLok

Answer y to add the marketplace, then choose a scope. Each mod is active immediately.

Settings

Mods with options show them as rows in /config, or you can set them in settings.json under pluginConfigs.<mod>.options. Changing one reloads the mod.


smart-model-router

Chooses a model for every turn and sends the main thread's requests to it. Subagents keep their own model.

How a model is chosen, in order:

  1. If your 5-hour or weekly usage is at or above the usage guard (default 85%), use the light model.
  2. If the mode is heavy or light, use that model.
  3. In auto mode, a prompt that mentions architecture, designing a system, multi-file work, rewriting the whole thing, deep reasoning, trade-offs, migrations, root cause, or planning, or one longer than 2,500 characters, gets the heavy model. Anything else gets the light model.

The status line shows the choice and the reason, e.g. router: claude-opus-5-5 (complex prompt).

Usage

CommandEffect
/routeShow the current mode
/route autoPick per prompt (default)
/route heavyAlways use the heavy model
/route lightAlways use the light model
/route offRouter does nothing; the session's own model is used

The mode is remembered across sessions.

Options

OptionDefaultMeaning
heavyModelclaude-opus-5-5Model for complex turns
lightModelclaude-sonnet-5-5Model for everyday turns
usageGuard85Usage % above which the light model is always used

Examples

PromptModel
Design the architecture for a multi-tenant billing serviceheavy
Plan out a migration from REST to gRPC, with the trade-offsheavy
Find the root cause of this flaky testheavy
fix the typo in READMElight
write a unit test for parseDatelight

Switching models starts a new prompt cache, so flipping often costs extra input tokens. Use /route heavy or /route light to pin a model for a stretch of work.


usage-bar

A band above the prompt with a meter for each rate-limit window Claude Code reports:

Usage 5h █████░░░░░ 42% 2h30m   week █████████░ 91% 3d
  • The bar is green below 70%, yellow from 70%, and red from 90%.
  • The trailing time is how long until the window resets.
  • A toast appears once when a window crosses 80% and once more at 95%.
  • The band uses figures Claude Code already receives, so it needs no server. It only appears on a subscription, and only after the first response brings rate-limit figures. It is hidden while a survey is showing.

Usage

CommandEffect
/usage-barHide or show the band (toggle)

Options

None.


context-bar

A stacked bar above the prompt showing the context window, one colour per /context category, with a legend of the biggest categories:

████████▒▒▒▒░░░░░░░░░░░░░░░░░░░░ 38% · 76k/200k
■ Messages 41k  ■ System tools 18k  ■ Memory files 6k
  • █ is used space, ▒ is the autocompact buffer, ░ is free space.
  • Figures are estimated locally from the last response's usage, so refreshing the bar sends no token-count requests. It refreshes after each turn and after a compaction.
  • It sits in the same band as usage-bar and stacks with it. It is hidden while a survey is showing.

Usage

CommandEffect
/context-barHide or show the bar (toggle)

Options

None.


research-offloader

When you type a research-style prompt, the mod keeps the page fetching and reading out of the main thread, so only a summary enters your context.

What counts as research: prompts containing "research", "look up", "summarize this url/page/article/docs", "compare sources", "read this page/article/docs", "what is the latest", or "search the web". It only reacts to prompts you type yourself.

Two modes, depending on the endpoint option:

  • Endpoint set: the mod POSTs { "query": "<your prompt>" } to it, expects { "summary": "..." } back, and gives Claude that summary as context. If the endpoint fails or returns no summary, it falls back to the subagent mode below.
  • Endpoint empty (default): the mod tells Claude to do the research inside subagents and return only short summaries with source URLs. For that turn, subagents spawned without a model run on the subagentModel.

A toast tells you which mode was used.

Options

OptionDefaultMeaning
endpointemptyURL of your own research bridge (for example a NotebookLM wrapper). Leave empty to use subagents
subagentModelhaikuModel for research subagents: haiku, sonnet or opus

Examples

PromptResult
Research the best Rust ORMs in 2026offloaded
Can you look up how Vite handles HMR?offloaded
summarize this page for me: https://example.comoffloaded
compare sources on WebGPU supportoffloaded
fix the failing test in auth.tsuntouched

NotebookLM has no public API, so the endpoint is yours to provide. Any service that takes {query} and returns {summary} works.


Bug fixes

0.1.3: usage-bar hid other bands above the prompt

  • Symptom: with usage-bar installed, a separate context bar disappeared.
  • Cause: both mods draw into the same band above the prompt (ui.render on AbovePrompt). The outermost hook returned its own tree without calling next(e), so the hook beneath it never ran.
  • Fix: usage-bar now calls next(e) and stacks what comes back under its own row. context-bar does the same from 0.1.4, so the two show together in either order.
  • For mod authors: a ui.render hook on a shared site must await next(e) and include the result in its tree. In tests, add a stand-in ui.render hook beneath the mod so next has something to return.

Development

Each mod has hooks/register.ts(x), a manifest in .claude-plugin/plugin.json, and tests in tests/.

claude plugin validate <mod folder>
claude plugin test <mod folder>

License

MIT

Source 2 files
hooks/register.tsx 91 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { UsageWindow } from '../types'
5
6const windows = atom({ plugin: 'usage-bar', key: 'windows' } as const, [])
7const isHidden = atom({ plugin: 'usage-bar', key: 'isHidden' } as const, false)
8
9const LABELS: Record<string, string> = { five_hour: '5h', seven_day: 'week', spend_limit: 'spend' }
10const WARN_AT = [80, 95]
11
12export const meter = (pct: number, width = 10): string => {
13  const filled = Math.max(0, Math.min(width, Math.round((pct / 100) * width)))
14
15  return '█'.repeat(filled) + '░'.repeat(width - filled)
16}
17
18export const colorFor = (pct: number) => (pct >= 90 ? 'error' : pct >= 70 ? 'warning' : 'success')
19
20const resetsIn = (iso: string | undefined, now: number): string => {
21  if (iso === undefined) return ''
22  const mins = Math.max(0, Math.round((Date.parse(iso) - now) / 60000))
23
24  return mins >= 60 * 24 ? ` ${Math.round(mins / 60 / 24)}d` : mins >= 60 ? ` ${Math.floor(mins / 60)}h${mins % 60}m` : ` ${mins}m`
25}
26
27export const register: Register = on => {
28  // Highest warning threshold already toasted per window, so each fires once.
29  const warned: Record<string, number> = {}
30
31  on('session.start', async ($, e, next) => {
32    await $.command.register({ name: 'usage-bar', description: 'Show or hide the usage band' })
33    const { rateLimits } = await $.session.usage()
34    await update($, windows, () => rateLimits)
35
36    return next(e)
37  })
38
39  on('command.run', { command: 'usage-bar' }, async $ => {
40    const hidden = await update($, isHidden, v => !v)
41
42    return { text: hidden ? 'Usage band hidden.' : 'Usage band shown.' }
43  })
44
45  on('session.measure', async ($, e, next) => {
46    if (e.changed.includes('rateLimits')) {
47      await update($, windows, () => e.rateLimits)
48      for (const w of e.rateLimits) {
49        const crossed = WARN_AT.filter(t => w.percentUsed >= t).pop()
50        if (crossed !== undefined && (warned[w.kind] ?? 0) < crossed) {
51          warned[w.kind] = crossed
52          $.ui.toast(`Claude ${LABELS[w.kind] ?? w.kind} usage at ${w.percentUsed}%`)
53        }
54      }
55    }
56
57    return next(e)
58  })
59
60  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
61    const list: UsageWindow[] = await read($, windows)
62    if (e.props.hasSurvey || list.length === 0 || (await read($, isHidden))) {
63      return next(e)
64    }
65
66    const { Box, Text } = $.ui.resolve(e)
67    const now = await $.clock.now()
68    // Other mods (e.g. context-bar) draw in this band too; answering without
69    // next() would replace them, so stack whatever sits beneath us.
70    const below = await next(e)
71
72    return (
73      <Box flexDirection="column">
74        <Box>
75          <Text dimColor>Usage </Text>
76          {list.map(w => (
77            <Text key={w.kind}>
78              <Text dimColor>{LABELS[w.kind] ?? w.kind} </Text>
79              <Text color={colorFor(w.percentUsed)}>
80                {meter(w.percentUsed)} {w.percentUsed}%
81              </Text>
82              <Text dimColor>{resetsIn(w.resetsAt, now)}   </Text>
83            </Text>
84          ))}
85        </Box>
86        {below}
87      </Box>
88    )
89  })
90}
91
types/index.d.ts 8 lines
1export type UsageWindow = { kind: string; percentUsed: number; resetsAt?: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'usage-bar': { windows: UsageWindow[]; isHidden: boolean }
6  }
7}
8