SLOPSHOPPER

usage-status

Mod that shows the remaining 5-hour session, 7-day weekly and context window usage as percentages in a row under the prompt, turning yellow when one runs low.

newspinner
★ 42v0.3.0no licenseupdated 2026-10-03robbyt/claude-skills/plugins/usage-status
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-status
› 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 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ⟨Claude Code's own drawing⟩ Claude Usage Remaining: session 69% · context 51%

Draws

Prompt hint
⟨Claude Code's own drawing⟩ Claude Usage Remaining: session 69% · context 51%
README

usage-status

A Claude Code mod that shows your remaining subscription and context usage as its own row under the prompt's hint line:

⏵⏵ auto mode on (shift+tab to cycle)
Claude Usage Remaining: session 94% · week 76% · context 63%
  • session is the rolling 5-hour window (five_hour)
  • week is the 7-day window (seven_day)
  • context is this session's context window, measured against the current model's window size

The row is drawn in the default color, and turns yellow when session or week drops below 10% remaining, or context below 15%. Auto-compact measures against its own limit, which can be smaller than the model's window, so it may fire while context still shows a little left.

How it works

The mod uses the function-hooks API (hooks/hooks.json → hooks/register.tsx):

  • A ui.render hook on PromptHint draws the engine's hint line, then a row below it with the rate-limit windows and context fill read from $.session.usage().
  • On session.measure, which fires after each turn and whenever a rate-limit window or the context fill moves, it calls $.ui.invalidate('ui.render') to redraw the line.

The figures come from the API's response to each turn, so the row appears after the first response of a session; context is also missing right after a compaction, until the next response. Off a subscription (API key, gateway) no rate-limit windows are reported and the row shows context alone.

hooks.json carries an empty "hooks": {} beside "modules" so skillsaw's claude-hooks-valid rule accepts it.

Install

/plugin install usage-status@robbyt-claude-skills

Or load it from a checkout:

claude --plugin-dir plugins/usage-status

Development

make test-plugin PLUGIN=usage-status

Runs claude plugin validate and claude plugin test against the plugin.

Source 2 files
hooks/register.tsx 35 lines
1import type { Register } from 'claude-code'
2
3import { formatUsage } from './format'
4
5export const register: Register = on => {
6  // Redraw the row whenever a rate-limit window or the context fill moves.
7  on('session.measure', ($, e, next) => {
8    if (e.changed.includes('rateLimits') || e.changed.includes('context')) {
9      $.ui.invalidate('ui.render')
10    }
11
12    return next(e)
13  })
14
15  // Draw the usage as its own row under the engine's hint line.
16  on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
17    const hint = await next(e)
18    const { rateLimits, context } = await $.session.usage()
19    const usage = formatUsage(rateLimits, context)
20
21    if (!usage) {
22      return hint
23    }
24
25    const { Box, Text } = $.ui.resolve(e)
26
27    return (
28      <Box flexDirection="column">
29        {hint}
30        <Text color={usage.isLow ? 'warning' : undefined}>{usage.text}</Text>
31      </Box>
32    )
33  })
34}
35
hooks/format.ts 46 lines
1import type { SessionContextUsage, SessionRateLimit } from 'claude-code'
2
3const LABELS: Record<string, string> = {
4  five_hour: 'session',
5  seven_day: 'week',
6}
7
8// Below this much remaining, the line turns yellow: a rate-limit window, or
9// the context window (higher, since auto-compact fires before it is full).
10export const LOW_LIMIT_PERCENT = 10
11export const LOW_CONTEXT_PERCENT = 15
12
13export type UsageLine = { text: string; isLow: boolean }
14
15type Remaining = { label: string; percent: number; low: number }
16
17const left = (percentUsed: number) =>
18  Math.max(0, Math.round(100 - percentUsed))
19
20// { text: "Claude Usage Remaining: session 77% · week 58% · context 63%", isLow },
21// or undefined when nothing has a reading yet.
22export const formatUsage = (
23  limits: readonly SessionRateLimit[],
24  context?: SessionContextUsage,
25): UsageLine | undefined => {
26  const parts: Remaining[] = Object.entries(LABELS).flatMap(([kind, label]) => {
27    const limit = limits.find(l => l.kind === kind)
28    return limit
29      ? [{ label, percent: left(limit.percentUsed), low: LOW_LIMIT_PERCENT }]
30      : []
31  })
32
33  if (context?.percent !== undefined) {
34    parts.push({ label: 'context', percent: left(context.percent), low: LOW_CONTEXT_PERCENT })
35  }
36
37  if (parts.length === 0) {
38    return undefined
39  }
40
41  return {
42    text: `Claude Usage Remaining: ${parts.map(p => `${p.label} ${p.percent}%`).join(' · ')}`,
43    isLow: parts.some(p => p.percent < p.low),
44  }
45}
46