Your plan's 5-hour and 7-day usage as small bars above the prompt, with the reset countdown and the session's cost.

<h1 align="center">Claude Code mods</h1>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT"></a> <a href="https://github.com/hamzafer/claude-code-mods/actions/workflows/ci.yml"><img src="https://github.com/hamzafer/claude-code-mods/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
<a href="#-install">Install</a> · <a href="#-the-mods">All mods</a> · <a href="docs/mods.md">Docs</a> · <a href="https://claude.dev/blog/getting-started-with-claude-code-mods/">What are mods?</a>
<table> <tr> <td align="center" width="33%"><a href="docs/mods.md#-context-bar"><img src="images/context-bar.png" alt="context-bar: Claude Code context window usage as a stacked bar, a color per category" width="260"></a><br>📊 <b>context-bar</b><br>what fills your context</td> <td align="center" width="33%"><a href="docs/mods.md#-review-watch"><img src="images/review-watch.png" alt="review-watch: live lines for running Codex and subagent code reviews in Claude Code" width="260"></a><br>🔍 <b>review-watch</b><br>running code reviews, live</td> <td align="center" width="33%"><a href="docs/mods.md#-md-preview"><img src="images/md-preview.png" alt="md-preview: Markdown that Claude Code edits, rendered like GitHub next to the diff" width="260"></a><br>📝 <b>md-preview</b><br>Markdown rendered like GitHub</td> </tr> <tr> <td align="center" width="33%"><a href="docs/mods.md#-blast-radius"><img src="images/gallery/blast-radius.png" alt="blast-radius: a Claude Code hook holds rm -rf and lists the files it would delete" width="260"></a><br>💥 <b>blast-radius</b><br>see what <code>rm -rf</code> would delete</td> <td align="center" width="33%"><a href="docs/mods.md#-now-playing"><img src="images/now-playing.png" alt="now-playing: Spotify track, progress bar and synced lyrics inside Claude Code" width="260"></a><br>🎵 <b>now-playing</b><br>Spotify and its lyrics, live</td> <td align="center" width="33%"><a href="docs/mods.md#-reels-and-snake"><img src="images/reels-demo.gif" alt="reels: YouTube Shorts in a Claude Code pane while it works" width="260"></a><br>📱 <b>reels</b><br>Shorts while Claude works</td> </tr> <tr> <td align="center" width="33%"><a href="docs/mods.md#-where-am-i"><img src="images/gallery/where-am-i.png" alt="where-am-i: the session goal, current step and what waits on you, above the Claude Code prompt" width="260"></a><br>📍 <b>where-am-i</b><br>goal, now, waiting on you</td> <td align="center" width="33%"><a href="docs/mods.md#-lines-above-the-prompt"><img src="images/gallery/lines.png" alt="token-weather, usage-meter and other Claude Code status lines stacked above the prompt" width="260"></a><br>🌦️ <b>token-weather and friends</b><br>lines above the prompt</td> <td align="center" width="33%"><a href="#mission-control"><img src="images/gallery/mission-control.png" alt="mission-control: Claude Code subagents, tool calls and the files they touch, live" width="260"></a><br>🛰️ <b>mission-control</b><br>agents and the code they touch</td> </tr> </table>
Add the marketplace once, then install any mod by name:
claude plugin marketplace add hamzafer/claude-code-mods
claude plugin install context-bar@claude-code-mods
Or install the general-purpose set in one go:
for m in context-bar token-weather usage-meter where-am-i next-steps agent-radar review-watch replay-theater md-preview blast-radius mission-control; do
claude plugin install "$m@claude-code-mods"
done
Restart Claude Code after installing. To try one without installing:
git clone https://github.com/hamzafer/claude-code-mods && cd claude-code-mods
claude --plugin-dir mods/context-bar
Needs Claude Code 2.1.287+. A few mods need more (Chrome,
gh, a connector); the tables say which.
| Mod | What it does | Command | |
|---|---|---|---|
| 🛰️ | mission-control | Live map of agents, tool calls and the code they touch | /mission |
| 📊 | context-bar | Your context window as one stacked bar, a color per category, with token counts and where it compacts | /context-bar |
| 🌦️ | token-weather | Context fill from Clear to Compact soon, plus a prompt-cache countdown | |
| ⏱️ | cache-clock | A prompt-cache line under your status line, from Claude Code's own figures: time left, hit rate and misses, and the tokens your next message re-caches once it goes cold. Needs Node and Claude Code 2.1.251+ | /cache-clock setup |
| 📍 | where-am-i | Goal, doing now, waiting on you, next step | /where |
| ➡️ | next-steps | 2 or 3 likely next prompts after each turn, one key to draft one | 1 2 3, 0 hides |
| 💰 | usage-meter | 5-hour and 7-day plan usage, the reset countdown and the session's cost | |
| 💳 | openai-balance | Your OpenAI API credit: an estimated balance with a gauge, today's spend, where the money mostly went, and the last call. Needs an OpenAI organization Admin key | /openai-balance |
| 📡 | agent-radar | One live line per running subagent | /radar |
| 🔍 | review-watch | One live line per running code review (Codex or a review subagent) with the model, target, elapsed time and Codex's latest output. A toast lists the findings when it ends | |
| 🌐 | browser-lanes | Whether this session has a browser, and who holds it | /browser |
| 🕌 | prayer-times | The current prayer and how long is left, the next one, and zawal. Computed on your computer, Hanafi or standard Asr | /prayers |
| 🎬 | replay-theater | Steps through the last turn's edits, one diff at a time | /replay |
| 📝 | md-preview | Renders the Markdown files Claude edits like GitHub does, with before and after side by side. Needs Chrome and a terminal that shows images | /md |
| Mod | What it does | Command | |
|---|---|---|---|
| 💥 | blast-radius | Holds rm -r, force pushes and migrations, shows what they'd delete, cancels after 60 s with no answer |
| Mod | What it does | Command | |
|---|---|---|---|
| 🔀 | switchboard | Picks the model for each subagent that doesn't name one, with OpenAI's Decisions API or Jev, from its short label only. Shows what every subagent cost | /route |
These are built around my own tools and rules. Fork them and change the rules to yours.
| Mod | What it does | Command | |
|---|---|---|---|
| 👀 | glance | One line with what needs you: next meeting, PRs, Linear issues, Slack DMs. Needs gh and the Google Calendar, Linear and Slack connectors | /glance |
| 🚦 | merge-gate | Holds gh pr merge until CI is green and Codex reviewed once. Needs gh and the Codex CLI. Reviews run on one fixed model; change it to yours | /gate |
| 📏 | rulebook-guard | Enforces my writing and git rules: rewrites em dashes, asks before --amend, unformatted pushes, emails and phone numbers in notes | |
| 💾 | session-saver | Saves where you left off, shows it on resume. Needs unpause | /park [note] |
| Mod | What it does | Command | |
|---|---|---|---|
| 📱 | reels | YouTube Shorts while Claude works, pauses when it's done | /reels |
| 🐍 | snake | Snake while Claude works | /snake |
| 🎵 | now-playing | What Spotify is playing, with a progress bar, the lyric being sung, and ⏮ ⏸ ⏭ buttons. Needs macOS and the Spotify app | /music |
<a id="mission-control"></a>
Every subagent, every tool call and every file they touch, in a pane next to the chat. Shown at 4x: two subagents building a logout feature across four files.

Install it like any mod, restart, and type /mission (or /mission code to open the code map). q closes it.
w shows the agents and every tool call, livec shows the code map, with import arrowsThe Code view also needs macOS, Google Chrome and a terminal that shows images (Ghostty, kitty, iTerm2). The Who view works everywhere.
hooks/register.tsx 205 lines1// Usage Meter: the plan's rate-limit windows and the session's cost, above the prompt.
2import { atom, read, update } from 'claude-code'
3import type { EngineInterface, Register, Timer } from 'claude-code'
4
5import type { UsageMeterSnapshot, UsageMeterWindow } from '../types'
6
7const CELLS = 5
8const BLOCKS = '▁▂▃▄▅▆▇█'
9const TICK_MS = 60_000
10const WARN_AT = 90
11const LABELS: Record<string, string> = { five_hour: '5h', seven_day: '7d', spend_limit: 'spend' }
12const ORDER = ['five_hour', 'seven_day', 'spend_limit']
13
14// Held by the host, so they survive a hot reload of this file.
15const snapshot = atom({ plugin: 'usage-meter', key: 'snapshot' } as const, null as UsageMeterSnapshot | null)
16const tick = atom({ plugin: 'usage-meter', key: 'tick' } as const, 0)
17const warned = atom({ plugin: 'usage-meter', key: 'warned' } as const, {} as Record<string, number>)
18
19type Segment = { text: string; color?: string; dim?: boolean }
20type Part = { kind: 'window' | 'cost'; isPrimary: boolean; segments: Segment[] }
21
22export const register: Register = on => {
23 let timer: Timer | undefined // one countdown timer, even if session.start fires again
24
25 on('session.start', async ($, e, next) => {
26 const result = await next(e)
27 // Moves the reset countdown on while a window has a reset time.
28 timer?.cancel()
29 timer = $.clock.every(TICK_MS, () => {
30 void (async () => {
31 const snap = await read($, snapshot)
32 if (snap?.rateLimits.some(w => w.resetsAt)) await update($, tick, n => n + 1)
33 })().catch(() => {})
34 })
35 const usage = await $.session.usage().catch(() => null)
36 if (usage) await take($, usage.rateLimits, usage.cost?.usd).catch(() => {})
37 return result
38 })
39
40 on('session.measure', async ($, e, next) => {
41 const result = await next(e)
42 await take($, e.rateLimits, e.cost?.usd).catch(() => {})
43 return result
44 })
45
46 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
47 const rest = await next(e) // what other mods and Claude Code draw here stays
48 await read($, tick) // redraws on each tick of the countdown
49 const snap = await read($, snapshot)
50 if (e.props.hasSurvey || !snap) return rest
51
52 const parts = fit(build(snap, await $.clock.now()), e.props.bodyColumns - 2)
53 if (parts.length === 0) return rest
54
55 const { Box, Text } = $.ui.resolve(e)
56 const segments: Segment[] = [{ text: '⏱ ', dim: true }]
57 parts.forEach((p, i) => {
58 if (i > 0) segments.push({ text: ' · ', dim: true })
59 segments.push(...p.segments)
60 })
61
62 return (
63 <Box flexDirection="column">
64 <Box flexDirection="row" paddingX={1}>
65 {segments.map((s, i) => (
66 <Text key={String(i)} color={s.color} dimColor={s.dim} wrap="truncate">
67 {s.text}
68 </Text>
69 ))}
70 </Box>
71 {rest}
72 </Box>
73 )
74 })
75}
76
77async function take($: EngineInterface, rateLimits: readonly UsageMeterWindow[], costUsd: number | undefined) {
78 const windows = rateLimits
79 .filter(w => Number.isFinite(w.percentUsed))
80 .map(w => ({ kind: w.kind, percentUsed: w.percentUsed, resetsAt: w.resetsAt }))
81 await update($, snapshot, () => ({ rateLimits: windows, costUsd }))
82 await warn($, windows)
83}
84
85// One toast per window per reset period, once it passes WARN_AT.
86async function warn($: EngineInterface, windows: UsageMeterWindow[]) {
87 const done = await read($, warned)
88 const now = await $.clock.now()
89 const fresh: Record<string, number> = {}
90 const rearm: string[] = []
91 for (const w of windows) {
92 const resetMs = resetTime(w)
93 if (w.percentUsed < WARN_AT) {
94 // With no reset time, dropping back under the line is the only sign of a new period.
95 if (resetMs === 0 && done[w.kind] !== undefined) rearm.push(w.kind)
96 continue
97 }
98 const last = done[w.kind]
99 // A reset time that moved on by more than half an hour is a new period.
100 if (last !== undefined && resetMs <= last + 30 * 60_000) continue
101 fresh[w.kind] = resetMs
102 const when = resetMs > 0 ? `, resets in ${countdown(resetMs - now)}` : ''
103 const what = w.kind === 'spend_limit' ? 'Spend limit' : `${label(w.kind)} usage limit`
104 $.ui.toast(`${what} at ${Math.round(w.percentUsed)}%${when}`)
105 }
106 if (Object.keys(fresh).length > 0 || rearm.length > 0) {
107 await update($, warned, old => {
108 const next = { ...old, ...fresh }
109 for (const kind of rearm) delete next[kind]
110 return next
111 })
112 }
113}
114
115export function build(snap: UsageMeterSnapshot, now: number): Part[] {
116 const windows = snap.rateLimits
117 .filter(w => !isStale(w, now)) // a window past its reset waits for a fresh reading
118 .sort((a, b) => rank(a.kind) - rank(b.kind))
119 const parts: Part[] = windows.map((w, i) => {
120 const color = colorFor(w.percentUsed)
121 const segments: Segment[] = [
122 { text: `${label(w.kind)} `, dim: true },
123 { text: bar(w.percentUsed), color },
124 { text: ` ${Math.round(w.percentUsed)}%`, color },
125 ]
126 const resetMs = resetTime(w)
127 // The first window always says when it resets; the others only once they run high.
128 if (resetMs > 0 && (i === 0 || w.percentUsed >= 75)) {
129 segments.push({ text: ` · resets ${countdown(resetMs - now)}`, dim: true })
130 }
131 return { kind: 'window', isPrimary: i === 0, segments }
132 })
133 if (snap.costUsd !== undefined && snap.costUsd > 0) {
134 parts.push({ kind: 'cost', isPrimary: false, segments: [{ text: `$${snap.costUsd.toFixed(2)} session`, dim: true }] })
135 }
136 return parts
137}
138
139// Drops the secondary windows first, then the cost, until the line fits.
140export function fit(parts: Part[], columns: number): Part[] {
141 let kept = [...parts]
142 const tooWide = () => width(kept) > columns
143 while (tooWide()) {
144 const i = findLastIndex(kept, p => p.kind === 'window' && !p.isPrimary)
145 if (i < 0) break
146 kept.splice(i, 1)
147 }
148 if (tooWide() && kept.some(p => p.isPrimary)) kept = kept.filter(p => p.kind !== 'cost')
149 return kept
150}
151
152function width(parts: Part[]) {
153 const text = parts.map(p => p.segments.map(s => s.text).join('')).join(' · ')
154 return 3 + [...text].length // the clock and its space: the glyph can draw two cells wide
155}
156
157function findLastIndex<T>(list: T[], test: (item: T) => boolean) {
158 for (let i = list.length - 1; i >= 0; i--) if (test(list[i] as T)) return i
159 return -1
160}
161
162export function bar(percent: number) {
163 const filled = (Math.min(Math.max(percent, 0), 100) / 100) * CELLS
164 let out = ''
165 for (let i = 0; i < CELLS; i++) {
166 const cell = Math.min(Math.max(filled - i, 0), 1)
167 out += BLOCKS[Math.round(cell * (BLOCKS.length - 1))]
168 }
169 return out
170}
171
172export function colorFor(percent: number) {
173 if (percent >= 90) return 'red'
174 if (percent >= 75) return 'yellow'
175 return 'green'
176}
177
178export function countdown(ms: number) {
179 const minutes = Math.max(0, Math.ceil(ms / 60_000))
180 if (minutes < 1) return 'now'
181 if (minutes < 60) return `${minutes}m`
182 const hours = Math.floor(minutes / 60)
183 if (hours < 24) return `${hours}h${String(minutes % 60).padStart(2, '0')}`
184 return `${Math.floor(hours / 24)}d${hours % 24}h`
185}
186
187function resetTime(w: UsageMeterWindow) {
188 const ms = w.resetsAt ? Date.parse(w.resetsAt) : NaN
189 return Number.isFinite(ms) ? ms : 0
190}
191
192function isStale(w: UsageMeterWindow, now: number) {
193 const resetMs = resetTime(w)
194 return resetMs > 0 && resetMs <= now
195}
196
197function label(kind: string) {
198 return LABELS[kind] ?? kind.replace(/_/g, ' ')
199}
200
201function rank(kind: string) {
202 const i = ORDER.indexOf(kind)
203 return i < 0 ? ORDER.length : i
204}
205types/index.d.ts 13 lines1export type UsageMeterWindow = { kind: string; percentUsed: number; resetsAt?: string }
2export type UsageMeterSnapshot = { rateLimits: UsageMeterWindow[]; costUsd?: number }
3
4declare module 'claude-code' {
5 interface PluginState {
6 'usage-meter': {
7 snapshot: UsageMeterSnapshot | null
8 tick: number
9 warned: Record<string, number>
10 }
11 }
12}
13