SLOPSHOPPER

usage-band

Rate limits, context, cost and git branch in a band above the prompt

newpanebandcommandtoastprocess
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-band
│ ┃ Usage ✕ › fix the failing auth test and add an audit log call │ ┃ Context │ ┃ No breakdown yet. ⏺ Read(src/auth.ts) │ ┃ ⎿ Read 6 lines │ ┃ Spend (sessions with this mod loaded) ⏺ Update(src/auth.ts) │ ┃ 2025-10-09 $0.42 ⎿ 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 │ ⎿ usage-band: Usage pane opened. │ │ ◔ 31% 5h · resets NaNm ◑ 49% ctx $0.42 $0.42 today $0.42 mo ⎇ feat/auth-refresh ±2 details ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
◔ 31% 5h · resets NaNm ◑ 49% ctx $0.42 $0.42 today $0.42 mo ⎇ feat/auth-refresh ±2 details
Pane · Usage
Context No breakdown yet. Spend (sessions with this mod loaded) 2025-10-09 $0.42
README

claude-mods

Three Claude Code mods: plugins built from function hooks that run inside Claude Code, in the terminal and the desktop app's Code tab.

ModWhat it does
house-rulesKeeps Claude's git work under your identity: removes AI co-author trailers, renames claude/ and ai/ branches, and blocks commits or PRs made as the wrong person
usage-bandShows 5-hour and 7-day usage limits, context fill, cost and the git branch in a band above the prompt
lean-commentsStops Claude adding banner, multi-line, long or narrating code comments, so comments only explain a non-obvious why in one short line

Needs Claude Code 2.1.286 or later.

Install

/plugin marketplace add WilsonKinyua/claude-mods
/plugin install house-rules@wilson-mods
/plugin install usage-band@wilson-mods
/plugin install lean-comments@wilson-mods
/reload-plugins

Plugins install at user scope, so they load in every project. Add --scope project to claude plugin install to turn one on for a single repo instead.

Mods run with the same access as Claude Code. Read the source before you install, as you would with any package.

house-rules

It checks every Bash command Claude runs, including subagents' commands, before the command executes.

Corrected automatically (you get a toast, and Claude is told what actually ran):

  • -c user.email=…, -c user.name=…, GIT_AUTHOR_* / GIT_COMMITTER_* env vars and any --author that isn't yours are removed.
  • Co-authored-by:, Generated-by:, 🤖 Generated with Claude Code and Claude session links are removed from commit messages and gh pr / gh issue text.
  • A new branch with a blocked prefix is renamed: git checkout -b claude/fix-login creates feat/fix-login, and a push in the same command follows the rename.

Blocked, with the reason given to Claude:

  • Pushing an existing branch with a blocked prefix, or opening a PR from one. Claude gets the git branch -m command to rename it first.
  • git commit in a repo whose user.email or user.name isn't yours.
  • git config user.email / user.name set to anything else.
  • gh pr create while gh is signed in to another account. This check only runs if you set a GitHub login.

Settings

Set these in /config, or from a terminal:

claude plugin configure house-rules@wilson-mods
SettingDefaultMeaning
gitEmailyour global user.emailThe only email commits may be authored as
gitNamesyour global user.nameAccepted author names, separated by commas
githubLoginempty (no check)The gh account allowed to open PRs
blockedBranchPrefixesclaude,ai,bot,copilot,codex,…Branch prefixes that get renamed or blocked
branchTypefeatThe prefix a blocked branch is renamed to

If one repo should keep a different identity, such as a work email, run this inside that repo yourself:

git config house-rules.allow-identity true

Claude can't set that key; the mod blocks it.

usage-band

usage-band in the desktop app: 5h and 7d limit rings with reset countdowns, context fill and cost

  • Rings for the 5-hour and 7-day limits, with reset countdowns. They turn amber at 60% and red at 85%.
  • Context window fill.
  • Cost of this session, plus today's and this month's totals.
  • Current git branch and the number of uncommitted changes.
  • A toast when a limit passes 80% and again at 90%.
  • /usage, or the details button, opens a pane with the context breakdown by category and spend per day.

The rings are drawn as SVG in the desktop app and as ◔◑◕ glyphs in the terminal. Limits only appear on a Claude subscription. The today and month totals only count sessions where the mod was loaded.

lean-comments

It checks every Edit and Write Claude makes. Only comments the change adds are checked; existing comments are left alone. By default a change is refused, with a list of the offending comments, and Claude rewrites it. A comment is flagged when it:

  • is a decorative banner or divider, such as // ── Section ────── or # ==========;
  • spans more than one line, including multi-line /** … */ doc blocks;
  • is longer than 120 characters;
  • narrates or labels the code, such as // Step 1: load config or # Helpers.

A change that adds more than 3 new comments is flagged too.

Tool directives are never flagged: eslint-disable, @ts-expect-error, noqa, # type: ignore, prettier-ignore, //go:, shebangs, license headers and similar. Markdown and other prose files are skipped.

It understands comment syntax for JS/TS, Go, Java, Kotlin, Swift, C/C++, C#, Rust, Dart, PHP, Python, Ruby, shell, YAML, TOML, SQL, Lua, CSS/SCSS, HTML, Vue, Svelte and Astro.

SettingDefaultMeaning
modeblockblock refuses the change so Claude rewrites it; warn lets it through and tells Claude what to fix; off disables the mod
maxLength120Longest comment text allowed, in characters
maxNewComments3How many new comments one change may add
allowDocCommentsfalseLet /** … */ doc blocks span several lines, e.g. for a published library's API

Develop

claude --plugin-dir ./plugins/usage-band
claude plugin validate ./plugins/usage-band

Saving a file reloads the mod in the running session. Bump version in the plugin's plugin.json and in .claude-plugin/marketplace.json when you release, so claude plugin marketplace update wilson-mods picks up the change.

License

MIT

Source 2 files
hooks/register.tsx 231 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { CostLedger, GitInfo, Limit, Snapshot, Totals } from '../types'
5
6const PANE = 'usage-details'
7const LEDGER_PREFIX = 'cost:'
8const KEEP_DAYS = 62
9
10const usage = atom({ plugin: 'usage-band', key: 'usage' } as const, null)
11const git = atom({ plugin: 'usage-band', key: 'git' } as const, null)
12const totals = atom({ plugin: 'usage-band', key: 'totals' } as const, null)
13const now = atom({ plugin: 'usage-band', key: 'now' } as const, 0)
14const warned = atom({ plugin: 'usage-band', key: 'warned' } as const, {})
15
16const GREEN = '#3fb950'
17const AMBER = '#d29922'
18const RED = '#f85149'
19const TRACK = 'rgba(128,128,128,0.35)'
20
21const tone = (percent: number) => (percent >= 85 ? RED : percent >= 60 ? AMBER : GREEN)
22const usd = (value: number) => `$${value < 10 ? value.toFixed(2) : value.toFixed(0)}`
23const pad = (n: number) => String(n).padStart(2, '0')
24const dayOf = (ms: number) => {
25  const d = new Date(ms)
26  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
27}
28
29const LABELS: Record<string, string> = { five_hour: '5h', seven_day: '7d', spend_limit: 'spend' }
30
31function countdown(resetsAt: string | undefined, at: number) {
32  if (!resetsAt) return ''
33  const minutes = Math.max(0, Math.round((Date.parse(resetsAt) - at) / 60000))
34  const days = Math.floor(minutes / 1440)
35  const hours = Math.floor((minutes % 1440) / 60)
36  if (days > 0) return `${days}d${hours}h`
37  return hours > 0 ? `${hours}h${minutes % 60}m` : `${minutes}m`
38}
39
40function ring(percent: number, color: string) {
41  const r = 9
42  const c = 2 * Math.PI * r
43  const filled = (Math.min(100, Math.max(0, percent)) / 100) * c
44  return `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22"><circle cx="11" cy="11" r="${r}" fill="none" stroke="${TRACK}" stroke-width="2.5"/><circle cx="11" cy="11" r="${r}" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round" stroke-dasharray="${filled} ${c}" transform="rotate(-90 11 11)"/></svg>`
45}
46
47const glyph = (percent: number) => (percent >= 88 ? '●' : percent >= 63 ? '◕' : percent >= 38 ? '◑' : percent >= 13 ? '◔' : '○')
48
49async function refreshGit($: EngineInterface) {
50  const run = (...args: string[]) => $.process.run(['git', ...args]).catch(() => null)
51  const [head, status] = await Promise.all([run('rev-parse', '--abbrev-ref', 'HEAD'), run('status', '--porcelain')])
52  const info: GitInfo | null =
53    head && head.exitCode === 0
54      ? { branch: head.stdout.trim(), changes: status?.stdout.split('\n').filter(Boolean).length ?? 0 }
55      : null
56  await update($, git, () => info)
57}
58
59export const register: Register = on => {
60  on('session.start', async ($, e, next) => {
61    const ran = await next(e)
62    await $.command.register({ name: 'usage', description: 'Show context and cost details in a pane' })
63    await update($, now, () => Date.now())
64
65    const first = await $.session.usage()
66    await update($, usage, () => ({
67      contextPercent: first.context.percent,
68      limits: first.rateLimits.map(({ kind, percentUsed, resetsAt }) => ({ kind, percentUsed, resetsAt })),
69      sessionUsd: first.cost?.usd,
70    }))
71    await refreshGit($)
72
73    $.clock.every(30_000, () => {
74      void update($, now, () => Date.now())
75      void refreshGit($)
76    })
77
78    return ran
79  })
80
81  on('session.measure', async ($, e, next) => {
82    const at = await $.clock.now()
83    const limits: Limit[] = e.rateLimits.map(({ kind, percentUsed, resetsAt }) => ({ kind, percentUsed, resetsAt }))
84    const snapshot: Snapshot = { contextPercent: e.context.percent, limits, sessionUsd: e.cost?.usd }
85    await update($, usage, () => snapshot)
86    await update($, now, () => at)
87
88    for (const limit of limits) {
89      const level = limit.percentUsed >= 90 ? 90 : limit.percentUsed >= 80 ? 80 : 0
90      const key = `${limit.kind}@${limit.resetsAt ?? ''}`
91      const seen = (await read($, warned))[key] ?? 0
92      if (level > seen) {
93        $.ui.toast(`${LABELS[limit.kind] ?? limit.kind} limit at ${limit.percentUsed}% · resets in ${countdown(limit.resetsAt, at)}`, { timeoutMs: 8000 })
94        await update($, warned, all => ({ ...all, [key]: level }))
95      }
96    }
97
98    if (e.cost) {
99      const sessionKey = `${LEDGER_PREFIX}${await $.session.id()}`
100      const ledger = ((await $.store.get(sessionKey)) as CostLedger | undefined) ?? { lastUsd: 0, days: {} }
101      const delta = Math.max(0, e.cost.usd - ledger.lastUsd)
102      const today = dayOf(at)
103      ledger.days[today] = (ledger.days[today] ?? 0) + delta
104      ledger.lastUsd = e.cost.usd
105      await $.store.set(sessionKey, ledger)
106
107      const cutoff = dayOf(at - KEEP_DAYS * 86_400_000)
108      const byDay: Record<string, number> = {}
109      for (const key of await $.store.keys()) {
110        if (!key.startsWith(LEDGER_PREFIX)) continue
111        const entry = (await $.store.get(key)) as CostLedger | undefined
112        const days = Object.entries(entry?.days ?? {})
113        if (days.every(([day]) => day < cutoff)) {
114          await $.store.delete(key)
115          continue
116        }
117        for (const [day, value] of days) byDay[day] = (byDay[day] ?? 0) + value
118      }
119      const month = today.slice(0, 7)
120      const sum: Totals = {
121        todayUsd: byDay[today] ?? 0,
122        monthUsd: Object.entries(byDay).reduce((acc, [day, value]) => (day.startsWith(month) ? acc + value : acc), 0),
123        days: Object.entries(byDay).sort(([a], [b]) => b.localeCompare(a)).slice(0, 14),
124      }
125      await update($, totals, () => sum)
126    }
127
128    return next(e)
129  })
130
131  on('turn.complete', async ($, e, next) => {
132    const ran = await next(e)
133    await refreshGit($)
134    return ran
135  })
136
137  on('command.run', { command: 'usage' }, async $ => {
138    await $.ui.open({ id: PANE, title: 'Usage' })
139    return { text: 'Usage pane opened.' }
140  })
141
142  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
143    const snapshot = await read($, usage)
144    if (e.props.hasSurvey || !snapshot) return next(e)
145
146    const [branch, sums, at] = await Promise.all([read($, git), read($, totals), read($, now)])
147    const elements = $.ui.resolve(e)
148    const { Box, Text, Button } = elements
149
150    const meter = (key: string, percent: number, label: string, detail: string) => {
151      const color = tone(percent)
152      return (
153        <Box key={key} flexDirection="row" alignItems="center" gap={1}>
154          {'Svg' in elements ? (
155            <elements.Svg source={ring(percent, color)} alt={`${label} ${percent}%`} width={22} height={22} />
156          ) : (
157            <Text color={color}>{glyph(percent)}</Text>
158          )}
159          <Text bold>{Math.round(percent)}%</Text>
160          <Text dimColor>{detail ? `${label} · ${detail}` : label}</Text>
161        </Box>
162      )
163    }
164
165    return (
166      <Box flexDirection="row" flexWrap="wrap" alignItems="center" columnGap={3} paddingX={1}>
167        {snapshot.limits.map(limit =>
168          meter(limit.kind, limit.percentUsed, LABELS[limit.kind] ?? limit.kind, limit.resetsAt ? `resets ${countdown(limit.resetsAt, at)}` : ''),
169        )}
170        {snapshot.contextPercent !== undefined && meter('context', snapshot.contextPercent, 'ctx', '')}
171        {snapshot.sessionUsd !== undefined && (
172          <Box key="cost" flexDirection="row" alignItems="center" gap={1}>
173            <Text bold color={GREEN}>{usd(snapshot.sessionUsd)}</Text>
174            {sums && <Text dimColor>{`${usd(sums.todayUsd)} today  ${usd(sums.monthUsd)} mo`}</Text>}
175          </Box>
176        )}
177        {branch && (
178          <Box key="git" flexDirection="row" alignItems="center" gap={1}>
179            <Text color="#58a6ff">⎇ {branch.branch}</Text>
180            {branch.changes > 0 && <Text color={AMBER}>±{branch.changes}</Text>}
181          </Box>
182        )}
183        <Button key="details" label="details" plain onPress={() => $.ui.open({ id: PANE, title: 'Usage' })} />
184      </Box>
185    )
186  })
187
188  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
189    const { Box, Text } = $.ui.resolve(e)
190    const [detail, sums] = await Promise.all([$.session.usage({ breakdown: 'summary' }), read($, totals)])
191    const breakdown = detail.context.breakdown
192
193    return (
194      <Box flexDirection="column" gap={1}>
195        <Box flexDirection="column">
196          <Text bold>Context</Text>
197          {breakdown ? (
198            <>
199              <Text dimColor>
200                {breakdown.totalTokens.toLocaleString()} / {breakdown.maxTokens.toLocaleString()} tokens ({Math.round(breakdown.percentage)}%)
201              </Text>
202              {breakdown.categories
203                .filter(category => category.tokens > 0)
204                .map(category => (
205                  <Text>
206                    <Text color={category.color}>■ </Text>
207                    {category.name}: {category.tokens.toLocaleString()}
208                  </Text>
209                ))}
210            </>
211          ) : (
212            <Text dimColor>No breakdown yet.</Text>
213          )}
214        </Box>
215        <Box flexDirection="column">
216          <Text bold>Spend (sessions with this mod loaded)</Text>
217          {sums && sums.days.length > 0 ? (
218            sums.days.map(([day, value]) => (
219              <Text>
220                {day}  {usd(value)}
221              </Text>
222            ))
223          ) : (
224            <Text dimColor>Nothing recorded yet.</Text>
225          )}
226        </Box>
227      </Box>
228    )
229  })
230}
231
types/index.d.ts 26 lines
1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2
3export type Snapshot = {
4  contextPercent?: number
5  limits: Limit[]
6  sessionUsd?: number
7}
8
9export type GitInfo = { branch: string; changes: number }
10
11export type Totals = { todayUsd: number; monthUsd: number; days: [string, number][] }
12
13export type CostLedger = { lastUsd: number; days: Record<string, number> }
14
15declare module 'claude-code' {
16  interface PluginState {
17    'usage-band': {
18      usage: Snapshot | null
19      git: GitInfo | null
20      totals: Totals | null
21      now: number
22      warned: Record<string, number>
23    }
24  }
25}
26