SLOPSHOPPER

hud

Right-docked pane: context used, session spend, remaining credit and clickable pending tasks that fill a ready-made prompt

newpanecommandnetwork
v0.1.0MITupdated 2026-10-09santisanudo13/claude-hud
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · hud
│ ┃ HUD ✕ › fix the failing auth test and add an audit log call │ ┃ ╭──────────────────────────────────────────╮ │ ┃ │ ◉ CONTEXT │ ⏺ Read(src/auth.ts) │ ┃ │ ████████░░░░░░░░ 49% │ ⎿ Read 6 lines │ ┃ │ 97.4k of 200k tokens │ ⏺ Update(src/auth.ts) │ ┃ ╰──────────────────────────────────────────╯ ⎿ Added 2 lines, removed 1 line │ ┃ ⏺ Bash(bun test) │ ┃ ╭──────────────────────────────────────────╮ ⎿ 3 pass, 1 fail │ ┃ │ ◆ SESSION │ │ ┃ │ $0.42 │ ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ │ spent this session │ │ ┃ │ 5h window: +0.0% │ ✻ Worked for 42s · done 4:20 PM │ ┃ ╰──────────────────────────────────────────╯ │ ┃ › /hud │ ┃ ╭──────────────────────────────────────────╮ ⎿ hud: HUD opened. │ ┃ │ ◈ CREDIT │ │ ┃ │ 5h window │ │ ┃ │ █████░░░░░░░░░░░ 31% │ │ ┃ ╰──────────────────────────────────────────╯ │ ┃ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · HUD
╭──────────────────────────────────────────────────────────╮ │ ◉ CONTEXT │ │ ████████░░░░░░░░ 49% │ │ 97.4k of 200k tokens │ ╰──────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────╮ │ ◆ SESSION │ │ $0.42 │ │ spent this session │ │ 5h window: +0.0% │ ╰──────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────╮ │ ◈ CREDIT │ │ 5h window │ │ █████░░░░░░░░░░░ 31% │ ╰──────────────────────────────────────────────────────────╯
README

claude-hud

A Claude Code mod: a right-docked pane showing

  • context window used (tokens and %)
  • session spend
  • remaining credit and rate-limit usage (5h window, 7d week)
  • clickable pending tasks that fill a ready-made prompt

Install

Inside Claude Code:

/plugin marketplace add santisanudo13/claude-hud
/plugin install hud@claude-hud

Then restart Claude Code (or run /reload-plugins).

If you already have a local copy of hud loaded (e.g. from ~/.claude/my-mods/hud), disable it first so the two don't clash.

Manual install

git clone https://github.com/santisanudo13/claude-hud ~/.claude/my-mods/hud

Then load it as a local mod from that folder (/plugin) and restart Claude Code.

Develop

Type definitions in .claude-plugin/types/ are generated by Claude Code and not committed. The mod lives in hooks/register.tsx.

License

MIT

Source 2 files
hooks/register.tsx 207 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Credit, Snapshot } from '../types'
5
6const PANE = 'hud'
7const snapshot = atom({ plugin: 'hud', key: 'snapshot' } as const, null)
8
9const WARN = 80
10const BAR = 16
11const REFRESH_MS = 60_000
12
13let credit: Credit | null = null
14let fetchedAt = 0
15
16const kilo = (n: number) => {
17  const k = Math.round(n / 100) / 10
18  if (n >= 999_950) {
19    return `${Math.round(n / 100_000) / 10}M`
20  }
21  return n >= 1000 ? `${k}k` : String(n)
22}
23const bar = (p: number) => {
24  const f = Math.max(0, Math.min(BAR, Math.round((p / 100) * BAR)))
25  return { on: '█'.repeat(f), off: '░'.repeat(BAR - f) }
26}
27const tone = (p: number) => (p >= 90 ? 'red' : p >= WARN ? 'yellow' : 'green')
28const money = (n: number) => `$${n.toFixed(2)}`
29const label = (k: string) =>
30  k === 'five_hour' ? '5h window' : k === 'seven_day' ? '7d week' : k === 'spend_limit' ? 'Credit' : k
31
32const take = (
33  e: {
34    context: { tokens?: number; window: number; percent?: number }
35    rateLimits: { kind: string; percentUsed: number }[]
36    cost?: { usd: number }
37  },
38  extra: Credit | null,
39  prev?: Snapshot | null,
40): Snapshot => ({
41  credit: extra,
42  tokens: e.context.tokens ?? null,
43  window: e.context.window,
44  percent: e.context.percent ?? null,
45  limits: e.rateLimits.map(l => ({ kind: l.kind, percentUsed: l.percentUsed })),
46  // usage of each window when this session first saw it; a window that renewed (value dropped) restarts from 0
47  base: e.rateLimits.map(l => {
48    const start = prev?.base?.find(b => b.kind === l.kind)?.percentUsed ?? l.percentUsed
49    return { kind: l.kind, percentUsed: l.percentUsed < start ? 0 : start }
50  }),
51  usd: e.cost?.usd ?? null,
52})
53
54const safe = async (fn: () => Promise<unknown>) => {
55  try {
56    await fn()
57  } catch {
58    // tracking must never break the tool call
59  }
60}
61
62// Same figures /usage shows; the credential never leaves the host.
63async function loadCredit($) {
64  const auth = await $.session.authorize()
65  if (!auth) {
66    return
67  }
68  const res = await $.http.fetch('https://api.anthropic.com/api/oauth/usage', {
69    auth: auth.handle,
70    headers: { 'anthropic-beta': 'oauth-2025-04-20' },
71  })
72  if (!res.ok) {
73    return
74  }
75  const spend = JSON.parse(res.text)?.spend
76  if (spend?.used && spend?.limit) {
77    const div = 10 ** (spend.limit.exponent ?? 2)
78    credit = { used: spend.used.amount_minor / div, limit: spend.limit.amount_minor / div, currency: spend.limit.currency }
79  }
80}
81
82async function refresh($) {
83  const now = Date.now()
84  if (now - fetchedAt >= REFRESH_MS) {
85    fetchedAt = now
86    await safe(() => loadCredit($))
87  }
88  const usage = await $.session.usage()
89  await update($, snapshot, prev => take(usage, credit, prev))
90}
91
92export const register: Register = on => {
93  on('session.start', async ($, e, next) => {
94    await $.command.register({ name: 'hud', description: 'Open the usage and context panel' })
95    await safe(() => refresh($))
96    void $.ui.open({ id: PANE, title: 'HUD' })
97
98    return next(e)
99  })
100
101  on('command.run', { command: 'hud' }, async $ => {
102    await $.ui.open({ id: PANE, title: 'HUD' })
103
104    return { text: 'HUD opened.' }
105  })
106
107  on('session.measure', async ($, e, next) => {
108    await update($, snapshot, prev => take(e, credit, prev))
109    void safe(() => refresh($))
110
111    return next(e)
112  })
113
114  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
115    const { Box, Text } = $.ui.resolve(e)
116    const s = await read($, snapshot)
117
118    const Meter = ({ percent }: { percent: number }) => {
119      const b = bar(percent)
120      return (
121        <Text>
122          <Text color={tone(percent)}>{b.on}</Text>
123          <Text dimColor>{b.off}</Text>
124          <Text bold color={tone(percent)}> {Math.round(percent)}%</Text>
125        </Text>
126      )
127    }
128
129    const Card = ({ title, color, right, children }: { title: string; color: string; right?: string; children: unknown }) => (
130      <Box flexDirection="column" borderStyle="round" borderColor={color} paddingX={1} marginBottom={1}>
131        <Box width="100%">
132          <Box flexGrow={1}>
133            <Text bold color={color}>
134              {title}
135            </Text>
136          </Box>
137          {right && <Text dimColor>{right}</Text>}
138        </Box>
139        {children}
140      </Box>
141    )
142
143    return (
144      <Box flexDirection="column">
145        <Card title="◉ CONTEXT" color="cyan">
146          {s?.percent != null ? (
147            <Box flexDirection="column">
148              <Meter percent={s.percent} />
149              <Text dimColor>
150                {s.tokens != null ? kilo(s.tokens) : '?'} of {kilo(s.window)} tokens
151              </Text>
152            </Box>
153          ) : (
154            <Text dimColor>waiting for first measurement…</Text>
155          )}
156        </Card>
157
158        <Card title="◆ SESSION" color="magenta">
159          <Text bold color="green">
160            {s?.usd != null ? money(s.usd) : '—'}
161          </Text>
162          <Text dimColor>spent this session</Text>
163          {(s?.limits ?? []).map(l => {
164            const used = Math.max(0, l.percentUsed - (s?.base?.find(b => b.kind === l.kind)?.percentUsed ?? l.percentUsed))
165            return (
166              <Text key={l.kind}>
167                <Text dimColor>{label(l.kind)}: </Text>
168                <Text bold color={tone(used * 4)}>
169                  +{used.toFixed(used < 10 ? 1 : 0)}%
170                </Text>
171              </Text>
172            )
173          })}
174        </Card>
175
176        <Card
177          title="◈ CREDIT"
178          color="yellow"
179        >
180          {s?.credit != null && (
181            <Box flexDirection="column">
182              <Meter percent={(s.credit.used / s.credit.limit) * 100} />
183              <Text>
184                <Text bold color={tone((s.credit.used / s.credit.limit) * 100)}>
185                  {money(s.credit.limit - s.credit.used)}
186                </Text>
187                <Text dimColor> left of {money(s.credit.limit)}</Text>
188              </Text>
189              <Text dimColor>month: {money(s.credit.used)} used</Text>
190            </Box>
191          )}
192          {(s?.limits ?? []).map(l => (
193            <Box key={l.kind} flexDirection="column">
194              <Box width="100%">
195                <Text dimColor>{label(l.kind)}</Text>
196              </Box>
197              <Meter percent={l.percentUsed} />
198            </Box>
199          ))}
200          {s?.credit == null && (s?.limits ?? []).length === 0 && <Text dimColor>no limit data</Text>}
201        </Card>
202
203      </Box>
204    )
205  })
206}
207
types/index.d.ts 20 lines
1export type Limit = { kind: string; percentUsed: number }
2export type Credit = { used: number; limit: number; currency: string }
3export type Snapshot = {
4  credit: Credit | null
5  tokens: number | null
6  window: number
7  percent: number | null
8  limits: Limit[]
9  base: Limit[]
10  usd: number | null
11}
12export type Status = 'pending' | 'in_progress' | 'completed'
13export type Item = { id: string; text: string; status: Status }
14
15declare module 'claude-code' {
16  interface PluginState {
17    hud: { snapshot: Snapshot | null }
18  }
19}
20