HUD above the prompt: model, project and git, context, and 5h and weekly limits.

A heads-up display for Claude Code, drawn as a band above the prompt. It is a rebuild of claude-hud as a Claude Code mod: it reads session data from the engine directly, so there is no statusLine script and no transcript parsing.
[Opus 5.5] │ my-app git:(main*)
Context █████░░░░░ 45% │ Usage ██░░░░░░░░ 25% (1h 30m / 5h) │ Weekly ████░░░░░░ 40% (3d 4h / 7d)
* when the working tree has changes).To collapse the band, press ctrl+x ctrl+a or select [-].
In a Claude Code terminal session, run:
/plugin install hud --marketplace mboisvertdupras/hud
Answer y to add the marketplace, then choose a scope.
If you use claude-hud, disable it and remove its statusLine entry from ~/.claude/settings.json. Otherwise both HUDs show.
claude --plugin-dir /path/to/hud
claude plugin validate /path/to/hud
claude plugin test /path/to/hud
The layout, symbols and color thresholds follow claude-hud by Jarrod Watts (MIT).
MIT
hooks/register.tsx 158 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, RenderChildren, SessionUsage } from 'claude-code'
3
4import type { HudSession, HudUsage, HudWindow } from '../types'
5
6const session = atom({ plugin: 'hud', key: 'session' } as const, null)
7const usage = atom({ plugin: 'hud', key: 'usage' } as const, null)
8const now = atom({ plugin: 'hud', key: 'now' } as const, 0)
9
10const displayModel = (model: string) => {
11 const match = /^claude-([a-z]+)-(\d+(?:-\d\b)?)/.exec(model)
12 if (!match?.[1] || !match[2]) return model
13 return `${match[1][0]?.toUpperCase()}${match[1].slice(1)} ${match[2].replace('-', '.')}`
14}
15
16const contextColor = (percent: number) => (percent >= 85 ? 'ansi256(1)' : percent >= 70 ? 'ansi256(3)' : 'ansi256(2)')
17
18const quotaColor = (percent: number) => (percent >= 90 ? 'ansi256(1)' : percent >= 75 ? 'ansi256(13)' : 'ansi256(12)')
19
20const bar = (percent: number, width: number) => {
21 const filled = Math.round((Math.min(100, Math.max(0, percent)) / 100) * width)
22 return ['█'.repeat(filled), '░'.repeat(width - filled)] as const
23}
24
25const formatResetTime = (resetsAt: string, at: number) => {
26 const minutes = Math.ceil((Date.parse(resetsAt) - at) / 60000)
27 if (minutes <= 0) return ''
28 if (minutes < 60) return `${minutes}m`
29 const hours = Math.floor(minutes / 60)
30 if (hours >= 24) return hours % 24 > 0 ? `${Math.floor(hours / 24)}d ${hours % 24}h` : `${Math.floor(hours / 24)}d`
31 return minutes % 60 > 0 ? `${hours}h ${minutes % 60}m` : `${hours}h`
32}
33
34const toUsage = ({ context, rateLimits }: Pick<SessionUsage, 'context' | 'rateLimits'>): HudUsage => {
35 const windowOf = (kind: string) => {
36 const limit = rateLimits.find(each => each.kind === kind)
37 return limit ? { percent: limit.percentUsed, resetsAt: limit.resetsAt } : null
38 }
39 return { contextPercent: context.percent ?? 0, fiveHour: windowOf('five_hour'), sevenDay: windowOf('seven_day') }
40}
41
42async function readSession($: EngineInterface): Promise<HudSession> {
43 const cwd = await $.session.cwd()
44 const [model, branch, status] = await Promise.all([
45 $.session.model(),
46 $.process.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], { cwd }),
47 $.process.run(['git', '--no-optional-locks', 'status', '--porcelain'], { cwd }),
48 ])
49 return {
50 model: displayModel(model),
51 project: cwd.split('/').filter(Boolean).pop() ?? '/',
52 git: branch.exitCode === 0 ? { branch: branch.stdout.trim(), isDirty: status.stdout.trim() !== '' } : null,
53 }
54}
55
56async function markNow($: EngineInterface) {
57 const at = await $.clock.now()
58 await update($, now, () => at)
59 return at
60}
61
62export const register: Register = on => {
63 on('session.start', async ($, e, next) => {
64 const started = await next(e)
65 const [current, figures] = await Promise.all([readSession($), $.session.usage(), markNow($)])
66 await update($, session, () => current)
67 await update($, usage, () => toUsage(figures))
68 return started
69 })
70
71 on('session.measure', async ($, e, next) => {
72 await markNow($)
73 await update($, usage, () => toUsage(e))
74 return next(e)
75 })
76
77 on('turn.complete', async ($, e, next) => {
78 const completed = await next(e)
79 if (e.agentId === undefined) {
80 const current = await readSession($)
81 await update($, session, () => current)
82 }
83 return completed
84 })
85
86 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
87 if (e.props.hasSurvey) return next(e)
88
89 const [current, figures, at] = await Promise.all([read($, session), read($, usage), read($, now)])
90 if (!current && !figures) return next(e)
91
92 const { Box, Text } = $.ui.resolve(e)
93 const width = e.props.bodyColumns
94 const barWidth = width >= 100 ? 10 : width >= 60 ? 6 : 4
95
96 const lines: [string, RenderChildren][] = []
97 if (current) {
98 lines.push([
99 'session',
100 [
101 <Text color="ansi256(6)">[{current.model}]</Text>,
102 <Text> │ </Text>,
103 <Text color="ansi256(3)">{current.project}</Text>,
104 current.git && (
105 <Text>
106 {' '}
107 <Text color="ansi256(5)">git:(</Text>
108 <Text color="ansi256(6)">
109 {current.git.branch}
110 {current.git.isDirty ? '*' : ''}
111 </Text>
112 <Text color="ansi256(5)">)</Text>
113 </Text>
114 ),
115 ],
116 ])
117 }
118 if (figures) {
119 const [contextFilled, contextEmpty] = bar(figures.contextPercent, barWidth)
120 const quotaSegment = (label: string, window: HudWindow | null, windowLabel: string) => {
121 if (!window) return null
122 const reset = window.resetsAt ? formatResetTime(window.resetsAt, at) : ''
123 return (
124 <Text>
125 <Text> │ </Text>
126 <Text dimColor>{label} </Text>
127 <Text color={quotaColor(window.percent)}>{bar(window.percent, barWidth)[0]}</Text>
128 <Text dimColor>{bar(window.percent, barWidth)[1]}</Text>
129 <Text color={quotaColor(window.percent)}> {window.percent}%</Text>
130 {reset && <Text> ({reset} / {windowLabel})</Text>}
131 </Text>
132 )
133 }
134 lines.push([
135 'usage',
136 [
137 <Text dimColor>Context </Text>,
138 <Text color={contextColor(figures.contextPercent)}>{contextFilled}</Text>,
139 <Text dimColor>{contextEmpty}</Text>,
140 <Text color={contextColor(figures.contextPercent)}> {figures.contextPercent}%</Text>,
141 quotaSegment('Usage', figures.fiveHour, '5h'),
142 quotaSegment('Weekly', figures.sevenDay, '7d'),
143 ],
144 ])
145 }
146
147 return (
148 <Box flexDirection="column" width={width}>
149 {lines.map(([key, line]) => (
150 <Box key={key}>
151 <Text wrap="truncate">{line}</Text>
152 </Box>
153 ))}
154 </Box>
155 )
156 })
157}
158types/index.d.ts 15 lines1export type HudGit = { branch: string; isDirty: boolean }
2export type HudSession = { model: string; project: string; git: HudGit | null }
3export type HudWindow = { percent: number; resetsAt?: string }
4export type HudUsage = { contextPercent: number; fiveHour: HudWindow | null; sevenDay: HudWindow | null }
5
6declare module 'claude-code' {
7 interface PluginState {
8 hud: {
9 session: HudSession | null
10 usage: HudUsage | null
11 now: number
12 }
13 }
14}
15