5-hour and weekly rate-limit gauges above the prompt, with a warning at 90%

Mods for Claude Code: small plugins that add to its screen and behavior. Each folder is one mod. Turn on the ones you want (see Setup) and change their settings in /config, under the mod's name.
Shows how full Claude's context window is, as a colored bar above the prompt.
/context-bar hides or shows it for this session.Shows how much of your 5-hour and weekly usage limits you've used, as two small meters under the context bar. They turn yellow at 70% and red at 90%, and you get a warning once a limit passes 90%.
Shows this session's ID in the bottom-right corner of the screen.
claude --resume <id> command.Adds a receipt to the line that closes each turn ("✻ Baked for 42s"): how many tools ran, which files were edited, the tokens used and what the turn cost.
Adds a one-sentence summary under Claude's longer replies, written by Claude Haiku.
/config sets how long a reply must be (600 characters by default).Lets you see images Claude reads, right in the conversation.
🖼 name.png · hover to preview. Point at it to show the picture, and move away to hide it.Gives Claude a voice, through murmur. It speaks only when you're away from the session (another app or tab in front), so it never talks over you while you're watching.
/read reads Claude's last reply aloud./tldr-aloud gives a spoken 2–3 sentence summary of the session so far./config./hush mutes this session; run it again to unmute./config under voice.Ties Claude into the Ghostty terminal. All its settings start with "Ghostty" in /config.
/title gets a new one right away. A resumed session gets its old title back./workspace saves and reopens whole windows of sessions: see Workspaces./goto (or ⌘⌃G) lists your open Claude sessions, the ones waiting on you first, and jumps to the one you pick. /goto next jumps straight to the one waiting longest; /goto <words> jumps to the one whose title matches./keybind add <keys> <text> makes a Ghostty key type something into Claude, like /keybind add super+ctrl+h /hush for ⌘⌃H. /keybind lists them, and /keybind remove <keys> removes one.A workspace is a Ghostty window of Claude sessions you can put away and bring back later. It remembers every tab and split: each Claude session with its conversation, and each shell with its folder. Reopening it brings back the whole window, with every conversation picking up where it left off.
| Command | What it does |
|---|---|
/workspace save [name] | Saves this window. With no name, it keeps the name the window already had, or Claude Haiku picks one from the tabs. |
/workspace close [name] | Saves this window, then exits every Claude session in it and closes their tabs. Your current tab stays open at the shell, and shell tabs stay open. |
/workspace list | Shows your saved workspaces, newest first. Press a number or Enter to reopen one, or Tab to ✕ delete to remove it. |
/workspace open <name> | Reopens a saved workspace in a new window. Part of the name is enough. |
claude --resume, so they continue their own conversation, with the tab title they had when you saved. A tab saved without a title gets a new one from its conversation. Shell tabs open at the prompt./workspace close. Pick it up tomorrow: /workspace list, then press its number.~/.claude/workspaces/, one JSON file per workspace.CLAUDE_CODE_PLUGIN_DIRS, separated by :, in the env block of ~/.claude/settings.json: "env": {
"CLAUDE_CODE_PLUGIN_DIRS": "/path/to/claude-mods/context-bar:/path/to/claude-mods/voice"
}
To try one mod for a single session: claude --plugin-dir /path/to/claude-mods/tldr.
CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1 in the same env block, so Claude Code doesn't overwrite the tab title.The first time a mod controls Ghostty, macOS asks for permission. Allow it.
Editing a mod's files reloads it in running sessions.
claude plugin validate context-bar # check a mod the way Claude Code will load it
claude plugin test context-bar # run its tests
IDEAS.md has ideas for more mods.
hooks/register.tsx 55 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Window } from '../types'
5import { color, label, meter, resets, toWarn } from './gauge'
6
7const windows = atom({ plugin: 'rate-limits', key: 'windows' } as const, [])
8const warned = atom({ plugin: 'rate-limits', key: 'warned' } as const, [])
9
10async function store($: EngineInterface, next: readonly Window[]) {
11 await update($, windows, () => next.map(w => ({ ...w })))
12 const fresh = toWarn(next, await read($, warned))
13 if (fresh.length === 0) return
14 await update($, warned, all => [...all, ...fresh].slice(-20))
15 for (const w of next.filter(w => fresh.includes(`${w.kind}@${w.resetsAt ?? ''}`)))
16 $.ui.toast(`⚠️ ${label(w.kind)} rate limit at ${w.percentUsed}%${w.resetsAt ? `, resets ${resets(w.resetsAt, Date.now())}` : ''}`)
17}
18
19export const register: Register = on => {
20 on('session.start', async ($, e, next) => {
21 await store($, (await $.session.usage()).rateLimits)
22 return next(e)
23 })
24
25 on('session.measure', async ($, e, next) => {
26 if (e.rateLimits.length) await store($, e.rateLimits)
27 return next(e)
28 })
29
30 // Stacks under what the plugins beneath draw (the context bar); nothing off a subscription.
31 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
32 const below = await next(e)
33 const list = await read($, windows)
34 if (e.props.hasSurvey || list.length === 0) return below
35
36 const { Box, Text } = $.ui.resolve(e)
37 const now = Date.now()
38 return (
39 <Box flexDirection="column">
40 {below}
41 <Text wrap="truncate">
42 {list.map((w, i) => (
43 <Text>
44 {i > 0 && <Text dimColor> </Text>}
45 <Text dimColor>{label(w.kind)} </Text>
46 <Text color={color(w.percentUsed)}>{meter(w.percentUsed)}</Text>
47 <Text dimColor> {w.percentUsed}%{w.resetsAt ? ` ↻ ${resets(w.resetsAt, now)}` : ''}</Text>
48 </Text>
49 ))}
50 </Text>
51 </Box>
52 )
53 })
54}
55hooks/gauge.ts 33 lines1import type { Window } from '../types'
2
3export const WARN_AT = 90
4
5export function label(kind: string): string {
6 return ({ five_hour: '5h', seven_day: 'week', spend_limit: 'spend' } as Record<string, string>)[kind] ?? kind
7}
8
9export function color(percent: number): string {
10 return percent >= WARN_AT ? 'red' : percent >= 70 ? 'yellow' : 'green'
11}
12
13export function meter(percent: number, width = 10): string {
14 const filled = Math.min(width, Math.max(0, Math.round((percent / 100) * width)))
15 return '▰'.repeat(filled) + '▱'.repeat(width - filled)
16}
17
18// "3:10 PM" for a reset within a day, "Mon 9 AM" beyond that.
19export function resets(iso: string | undefined, now: number): string {
20 if (!iso) return ''
21 const at = new Date(iso)
22 const isSoon = at.getTime() - now < 24 * 3600 * 1000
23 return at.toLocaleString([], isSoon ? { hour: 'numeric', minute: '2-digit' } : { weekday: 'short', hour: 'numeric' })
24}
25
26// The window keys that just crossed WARN_AT and haven't been warned about since they last reset.
27export function toWarn(windows: readonly Window[], warned: readonly string[]): string[] {
28 return windows
29 .filter(w => w.percentUsed >= WARN_AT)
30 .map(w => `${w.kind}@${w.resetsAt ?? ''}`)
31 .filter(k => !warned.includes(k))
32}
33types/index.d.ts 8 lines1export type Window = { kind: string; percentUsed: number; resetsAt?: string }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'rate-limits': { windows: Window[]; warned: string[] }
6 }
7}
8