SLOPSHOPPER

usage-buddy

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…

newband
v0.1.0MITupdated 2026-10-19Surigoma001/claude-usage-buddy
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-buddy
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ 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 ▐▛███▜▌ ▝▜█████▛▘ ▘▘ ▝▝ 使用量を開閉 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
▐▛███▜▌ ▝▜█████▛▘ ▘▘ ▝▝ 使用量を開閉
README

English | 日本語

usage-buddy

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

Why

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.

Features

  • Stays small in narrow panes. The mascot and the number fit in a fixed 10-cell column (the widest value, 100%/4h59m, is exactly 10 cells). Lines are never wrapped. If the pane is narrower than 10 cells, nothing is drawn at all.
  • Click to expand. Clicking the number toggles a detail list: 5-hour limit (with a small bar), weekly limit, and context window usage. Click again to close it.
  • Mood by color. The mascot's body changes color as usage climbs, and its eyes droop when things get tight.
  • Follows your theme. Colors use Claude Code's theme colors (claude / warning / error), so they adapt to light, dark and color-blind-friendly themes.

States

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.

UsageBody colorEyesLooks like
under 60%Claude orangenormal ▐▛███▜▌
60% to under 85%yellow (warning)normal ▐▛███▜▌
85% and abovered (error)half-closed ▐▀███▀▌

Once a limit's reset time has passed, it is treated as 0% used until the next response brings fresh numbers.

Detail view (when opened)

 ▐▛███▜▌
▝▜█████▛▘
  ▘▘ ▝▝
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.

Install

At the prompt of a Claude Code session in a terminal, type:

/plugin install usage-buddy --marketplace Surigoma001/claude-usage-buddy

Then:

  1. When asked Add marketplace? (source github:Surigoma001/claude-usage-buddy), answer y.
  2. Choose a scope. The user scope is listed first; press Enter to install it for all your sessions.
  3. You should see 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 and uninstall

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

Requirements and limitations

  • Subscription plans only (Pro / Max). The 5-hour and weekly limits are only reported when you sign in with a Claude subscription. With pay-as-you-go API billing there are no such limits, so only the mascot is drawn: no number appears, and because the number is the click target, the detail view cannot be opened either. (The mascot's color still reacts to context usage.)
  • Nothing until the first response. The numbers come from API responses, so the mascot is hidden until Claude has replied at least once in the session.
  • No credit balance. Cloud / API credit balance is not available through the data Claude Code exposes, so it cannot be shown.
  • VS Code: use the integrated terminal. It works when Claude Code runs as a CLI in VS Code's integrated terminal. It is not shown in the VS Code extension's chat panel.
  • Clicking may require fullscreen mode (unverified). Mouse clicks in the terminal may only reach the mod when Claude Code is in its fullscreen display mode. This has not been confirmed. If clicking does nothing, the compact number still works.
  • Desktop app: untested. Claude Code's docs say a mod installed at the user scope also loads in the desktop app's local Code sessions, but this mod has not been tested there.
  • Install command is terminal-only. /plugin install is not available in the desktop app's Code tab; run it from a terminal session.

Customization

Everything lives in hooks/register.tsx. Fork the repository, edit, and load your copy (see Development).

WhatWhere
Colors per moodBODY (theme color names: claude, warning, error, ...)
Eyes per moodEYES (left/right eye characters)
Thresholds (60% / 85%)moodOf
Width of the columnMASCOT_WIDTH (default 10; the narrowest pane in which anything is drawn)
Mascot shapethe three <Text> lines near the end of the ui.render hook
Detail labels and languagethe 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.

Development and testing

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.

License

MIT © 2026 Shun Inc.

This is an unofficial community mod and is not affiliated with or endorsed by Anthropic.

Source 2 files
hooks/register.tsx 167 lines
1import { 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}
167
types/index.d.ts 12 lines
1export 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