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

Three Claude Code mods: plugins built from function hooks that run inside Claude Code, in the terminal and the desktop app's Code tab.
| Mod | What it does |
|---|---|
| house-rules | Keeps 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-band | Shows 5-hour and 7-day usage limits, context fill, cost and the git branch in a band above the prompt |
| lean-comments | Stops 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.
/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.
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.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:
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.Set these in /config, or from a terminal:
claude plugin configure house-rules@wilson-mods
| Setting | Default | Meaning |
|---|---|---|
gitEmail | your global user.email | The only email commits may be authored as |
gitNames | your global user.name | Accepted author names, separated by commas |
githubLogin | empty (no check) | The gh account allowed to open PRs |
blockedBranchPrefixes | claude,ai,bot,copilot,codex,… | Branch prefixes that get renamed or blocked |
branchType | feat | The 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, 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.
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:
// ── Section ────── or # ==========;/** … */ doc blocks;// 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.
| Setting | Default | Meaning |
|---|---|---|
mode | block | block refuses the change so Claude rewrites it; warn lets it through and tells Claude what to fix; off disables the mod |
maxLength | 120 | Longest comment text allowed, in characters |
maxNewComments | 3 | How many new comments one change may add |
allowDocComments | false | Let /** … */ doc blocks span several lines, e.g. for a published library's API |
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.
hooks/register.tsx 231 lines1import { 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}
231types/index.d.ts 26 lines1export 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