A tiny Claude-style mascot above the prompt that shows your 5-hour limit remaining and time to reset (e.g. 93%/4h51m). Click to expand 5-hour, weekly and…

English | 日本語
A tiny buddy above your prompt that tells you how much of your Claude limit is left.
usage-buddy is a mod (plugin) for Claude Code. It sits just above the input box, aligned to the right, and shows a small Claude-style mascot with your 5-hour limit remaining and the time until it resets underneath it.
▐▛███▜▌
▝▜█████▛▘
▘▘ ▝▝
93%/4h51m
If you split VS Code into three vertical panes and run Claude Code in one of them, a full status line gets cut off or wraps and becomes hard to read. usage-buddy was designed for exactly that layout: it is only 10 cells wide, never wraps, and tells you at a glance whether you can keep going or should slow down.
100%/4h59m, is exactly 10 cells). Lines are never wrapped. If the pane is narrower than 10 cells, nothing is drawn at all.claude / warning / error), so they adapt to light, dark and color-blind-friendly themes.The mascot's mood is based on the highest of: 5-hour usage, weekly usage, and context usage. The number's color is based on the 5-hour limit only.
| Usage | Body color | Eyes | Looks like |
|---|---|---|---|
| under 60% | Claude orange | normal | ▐▛███▜▌ |
| 60% to under 85% | yellow (warning) | normal | ▐▛███▜▌ |
| 85% and above | red (error) | half-closed | ▐▀███▀▌ |
Once a limit's reset time has passed, it is treated as 0% used until the next response brings fresh numbers.
▐▛███▜▌
▝▜█████▛▘
▘▘ ▝▝
93%/4h51m
5時間 █░░░░░░░ 残93% 4時間51分後
週間 残71% 3日2時間後
ctx 24%
Note: in version 0.1.0 the detail labels are in Japanese (5時間 = 5-hour, 週間 = weekly, 残 = remaining, 後 = from now). The compact number (93%/4h51m) is ASCII only. See Customization to change the labels.
At the prompt of a Claude Code session in a terminal, type:
/plugin install usage-buddy --marketplace Surigoma001/claude-usage-buddy
Then:
Add marketplace? (source github:Surigoma001/claude-usage-buddy), answer y.Installed usage-buddy. Plugin is now active. The mascot appears after Claude's first response in the session.If you have already added this marketplace, step 1 is skipped.
Update to the latest version:
claude plugin update usage-buddy@claude-usage-buddy
Then restart Claude Code, or run /reload-plugins in an open session.
Uninstall:
claude plugin uninstall usage-buddy@claude-usage-buddy
To also remove the marketplace entry:
claude plugin marketplace remove claude-usage-buddy
/plugin install is not available in the desktop app's Code tab; run it from a terminal session.Everything lives in hooks/register.tsx. Fork the repository, edit, and load your copy (see Development).
| What | Where |
|---|---|
| Colors per mood | BODY (theme color names: claude, warning, error, ...) |
| Eyes per mood | EYES (left/right eye characters) |
| Thresholds (60% / 85%) | moodOf |
| Width of the column | MASCOT_WIDTH (default 10; the narrowest pane in which anything is drawn) |
| Mascot shape | the three <Text> lines near the end of the ui.render hook |
| Detail labels and language | the details.push(...) lines and the until function |
If you change the mascot or the number format, keep every line within MASCOT_WIDTH cells so it does not wrap in narrow panes.
git clone https://github.com/Surigoma001/claude-usage-buddy.git
cd claude-usage-buddy
claude plugin validate . # checks the marketplace file, manifest and hooks module
claude plugin test . # runs tests/*.test.tsx against the engine
claude --plugin-dir . # try it in a session without installing
Repository layout:
.claude-plugin/
marketplace.json # makes this repository a marketplace (source "./")
plugin.json # the mod's manifest
hooks/
hooks.json # points at register.tsx
register.tsx # the mod itself
types/index.d.ts # state typings
tests/band.test.tsx # tests
Issues and pull requests are welcome.
MIT © 2026 Shun Inc.
This is an unofficial community mod and is not affiliated with or endorsed by Anthropic.
hooks/register.tsx 167 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Limit, Snapshot } from '../types'
5
6const snapshot = atom({ plugin: 'usage-buddy', key: 'snapshot' } as const, null)
7const isOpen = atom({ plugin: 'usage-buddy', key: 'isOpen' } as const, false)
8
9type Mood = 'ok' | 'warn' | 'danger'
10
11// 体の色はテーマに追従する(ライト/ダーク/色覚配慮)
12const BODY: Record<Mood, string> = { ok: 'claude', warn: 'warning', danger: 'error' }
13// 目: 平常は本物どおり。ピンチだけ半目にして、色以外でも差が出るようにする
14const EYES: Record<Mood, [string, string]> = {
15 ok: ['▛', '▜'],
16 warn: ['▛', '▜'],
17 danger: ['▀', '▀'],
18}
19
20const MASCOT_WIDTH = 10 // 縦積み。「100%/4h59m」(10セル)がこの幅に収まる
21
22const moodOf = (usedPercent: number): Mood =>
23 usedPercent >= 85 ? 'danger' : usedPercent >= 60 ? 'warn' : 'ok'
24
25const bar = (usedPercent: number, width: number): string => {
26 const filled = Math.min(width, Math.round((usedPercent / 100) * width))
27 return '█'.repeat(filled) + '░'.repeat(width - filled)
28}
29
30const isPast = (iso: string | undefined, nowMs: number): boolean =>
31 iso !== undefined && Date.parse(iso) <= nowMs
32
33// リセット時刻を過ぎた枠は、次の応答が来るまで古い値が残るので 0% 扱いにする
34const usedOf = (l: Limit, nowMs: number): number =>
35 isPast(l.resetsAt, nowMs) ? 0 : l.percentUsed
36
37const until = (iso: string | undefined, nowMs: number): string => {
38 if (!iso) return ''
39 const diff = Date.parse(iso) - nowMs
40 if (!Number.isFinite(diff)) return ''
41 if (diff <= 0) return 'リセット済'
42 const minutes = Math.floor(diff / 60000)
43 const days = Math.floor(minutes / 1440)
44 const hours = Math.floor((minutes % 1440) / 60)
45 if (days > 0) return `${days}日${hours}時間後`
46 if (hours > 0) return `${hours}時間${minutes % 60}分後`
47 return `${minutes}分後`
48}
49
50// 数字の行用の短い残り時間: 4h51m / 51m / 3d(ASCIIだけなので幅がずれない)
51const shortUntil = (iso: string | undefined, nowMs: number): string => {
52 if (!iso) return ''
53 const diff = Date.parse(iso) - nowMs
54 if (!Number.isFinite(diff) || diff <= 0) return '0m'
55 const minutes = Math.floor(diff / 60000)
56 const days = Math.floor(minutes / 1440)
57 if (days > 0) return `${days}d`
58 const hours = Math.floor(minutes / 60)
59 return hours > 0 ? `${hours}h${String(minutes % 60).padStart(2, '0')}m` : `${minutes}m`
60}
61
62const left = (usedPercent: number): number =>
63 Math.max(0, Math.min(100, Math.round(100 - usedPercent)))
64
65const toSnapshot = (
66 contextPercent: number | null | undefined,
67 limits: readonly Limit[],
68): Snapshot => ({
69 contextPercent: contextPercent ?? null,
70 limits: limits.map(l => ({ kind: l.kind, percentUsed: l.percentUsed, resetsAt: l.resetsAt })),
71})
72
73export const register: Register = on => {
74 on('session.start', async ($, e, next) => {
75 const usage = await $.session.usage()
76 await update($, snapshot, () => toSnapshot(usage.context.percent, usage.rateLimits))
77
78 return next(e)
79 })
80
81 on('session.measure', async ($, e, next) => {
82 await update($, snapshot, () => toSnapshot(e.context.percent, e.rateLimits))
83
84 return next(e)
85 })
86
87 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
88 const snap = await read($, snapshot)
89 const width = e.props.bodyColumns
90
91 // アンケート中・測定前・幅が足りないときは、何も描かずエンジンに譲る
92 if (e.props.hasSurvey || snap === null || width < MASCOT_WIDTH) {
93 return next(e)
94 }
95
96 const { Box, Text, Button } = $.ui.resolve(e)
97 const open = await read($, isOpen)
98 const now = await $.clock.now()
99 const five = snap.limits.find(l => l.kind === 'five_hour')
100 const week = snap.limits.find(l => l.kind === 'seven_day')
101
102 // キャラの気分は、全枠とコンテキストのうち一番高い値。数字の色は5時間枠だけで決める
103 const worst = Math.max(
104 snap.contextPercent ?? 0,
105 ...snap.limits.map(l => usedOf(l, now)),
106 )
107 const fiveUsed = five === undefined ? 0 : usedOf(five, now)
108 const body = BODY[moodOf(worst)]
109 const numColor = BODY[moodOf(fiveUsed)]
110 const [eyeL, eyeR] = EYES[moodOf(worst)]
111 const time = five === undefined ? '' : shortUntil(five.resetsAt, now)
112 const rest =
113 five === undefined ? '' : time === '' ? `${left(fiveUsed)}%` : `${left(fiveUsed)}%/${time}`
114
115 const details: string[] = []
116 if (open) {
117 if (five) {
118 details.push(`5時間 ${bar(fiveUsed, 8)} 残${left(fiveUsed)}% ${until(five.resetsAt, now)}`)
119 }
120 if (week) {
121 details.push(`週間 残${left(usedOf(week, now))}% ${until(week.resetsAt, now)}`)
122 }
123 if (snap.contextPercent !== null) {
124 details.push(`ctx ${Math.round(snap.contextPercent)}%`)
125 }
126 }
127
128 // 押せるのは Button だけ。子は Text のみ許される(Box は拒否される)
129 const toggle =
130 rest === '' ? null : (
131 <Button
132 key="toggle"
133 plain
134 label="使用量を開閉"
135 onPress={() => update($, isOpen, v => !v)}
136 >
137 <Text color={numColor} bold>
138 {rest}
139 </Text>
140 </Button>
141 )
142
143 const detailRows = details.map(d => (
144 <Text key={d} dimColor wrap="truncate-end">
145 {d}
146 </Text>
147 ))
148
149 // 縦積み: 上にキャラ、すぐ下に残り%(幅は MASCOT_WIDTH セルで固定)。詳細は開いたときだけ
150 return (
151 <Box flexDirection="column" alignItems="flex-end">
152 <Box flexDirection="column" flexShrink={0} width={MASCOT_WIDTH}>
153 <Text color={body}>{` ▐${eyeL}███${eyeR}▌`}</Text>
154 <Text color={body}>{'▝▜█████▛▘'}</Text>
155 <Text color={body}>{' ▘▘ ▝▝'}</Text>
156 </Box>
157 {toggle !== null && (
158 <Box width={MASCOT_WIDTH} justifyContent="center" flexShrink={0}>
159 {toggle}
160 </Box>
161 )}
162 {detailRows}
163 </Box>
164 )
165 })
166}
167types/index.d.ts 12 lines1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2export type Snapshot = {
3 contextPercent: number | null
4 limits: Limit[]
5}
6
7declare module 'claude-code' {
8 interface PluginState {
9 'usage-buddy': { snapshot: Snapshot | null; isOpen: boolean }
10 }
11}
12