HUD for Claude Code

🚧 Work in progress: plugins and their interfaces may change without notice.
A Claude Code marketplace of mods by Nadun De Silva.
| Plugin | What it does |
|---|---|
| hud | Draws a HUD above the prompt: model, context usage, rate limits, working directory and git ref |
First add the marketplace, then install the plugins you want from it. At the prompt of a terminal session:
/plugin marketplace add nadundesilva/claude-plugins
/plugin install <plugin-name>@nadundesilva
Or from a shell:
claude plugin marketplace add nadundesilva/claude-plugins
claude plugin install <plugin-name>@nadundesilva
To pick up new plugins and updates later, run /plugin marketplace update nadundesilva (or claude plugin marketplace update nadundesilva).
/plugin install <plugin-name> --marketplace nadundesilva/claude-plugins does both steps in one go, asking you to confirm adding the marketplace.
claude CLI (2.1.300 or later), logged inpnpm i
This installs the dev tooling and generates the 'claude-code' types into each plugin's .claude-plugin/types/ (gitignored). The generation runs claude headlessly once per plugin and makes one tiny model call each.
claude --plugin-dir plugins/<plugin-name>
The plugin hot-reloads when you save a file.
Each script runs against every folder in plugins/.
| Script | What it does |
|---|---|
pnpm types:gen | Regenerates .claude-plugin/types/ (run it after upgrading Claude Code) |
pnpm types:check | Type-checks each plugin with tsc |
pnpm lint | Lints the repo with ESLint |
pnpm validate | Validates the marketplace and each plugin with claude plugin validate |
pnpm test | Runs each plugin's tests/ with claude plugin test |
pnpm format | Formats the repo with Prettier |
pnpm format:check | Checks formatting |
A husky pre-commit hook runs format:check, lint, types:check, validate and test. pnpm i installs it.
.claude-plugin/marketplace.json: lists the pluginsplugins/<name>/: one self-contained plugin.claude-plugin/plugin.json: the plugin manifesthooks/hooks.json: lists the hooks moduleshooks/*.tsx: the hookstests/: unit teststsconfig.json: extends the generated .claude-plugin/types/tsconfig.jsonCreate plugins/<name>/ with the layout above, add it to .claude-plugin/marketplace.json and run pnpm i to generate its types. The scripts and CI pick it up from the plugins/* glob.
hooks/register.tsx 25 lines1import type { Register } from "claude-code";
2import { REFRESH_MS, refresh, renderHud } from "./hud";
3
4export const register: Register = (on) => {
5 on("session.start", async ($, e, next) => {
6 $.clock.every(REFRESH_MS, () => refresh($));
7 await refresh($);
8 return next(e);
9 });
10
11 on("session.measure", async ($, e, next) => {
12 await refresh($);
13 return next(e);
14 });
15
16 on("turn.complete", async ($, e, next) => {
17 await refresh($);
18 return next(e);
19 });
20
21 on("ui.render", { component: "PromptHint" }, async ($, e, next) =>
22 renderHud($, e, await next(e)),
23 );
24};
25hooks/hud.tsx 137 lines1import type { EngineInterface, RenderElement, RenderInput } from "claude-code";
2
3export const REFRESH_MS = 60_000;
4
5// Row colors, as hex.
6const COLORS = {
7 cwd: "#87afd7",
8 git: "#d7af5f",
9 model: "#5fafaf",
10 context: "#af87af",
11 limits: "#d75f5f",
12 separator: "#585858",
13};
14
15type Hud = {
16 model: string;
17 context?: number;
18 fiveHour: string;
19 sevenDay: string;
20 cwd: string;
21 ref: string;
22};
23
24let hud: Hud | undefined;
25
26export const modelName = (id: string) => {
27 const match = /^claude-([a-z]+)-(\d+)(?:-(\d{1,2})\b)?/.exec(id);
28 if (!match) return id;
29 const [, family = "", major = "", minor] = match;
30 const name = family.charAt(0).toUpperCase() + family.slice(1);
31 return `${name} ${major}${minor ? `.${minor}` : ""}`;
32};
33
34export const formatRemaining = (secs: number) => {
35 if (secs <= 0) return "now";
36 const days = Math.floor(secs / 86400);
37 const hrs = Math.floor((secs % 86400) / 3600);
38 const mins = Math.floor((secs % 3600) / 60);
39 if (days > 0) return `${days}d ${hrs}h ${mins}m`;
40 if (hrs > 0) return `${hrs}h ${mins}m`;
41 return mins > 0 ? `${mins}m` : "<1m";
42};
43
44// Branch, else exact tag, else short sha.
45async function gitRef($: EngineInterface, cwd: string) {
46 const git = async (...args: string[]) => {
47 try {
48 const { exitCode, stdout } = await $.process.run(
49 ["git", "--no-optional-locks", ...args],
50 { cwd },
51 );
52 return exitCode === 0 ? stdout.trim() : "";
53 } catch (error) {
54 // A missing git binary or a timeout must not take the whole HUD down.
55 $.ui.log(`git ${args[0]} failed: ${String(error)}`);
56 return "";
57 }
58 };
59 const branch = await git("symbolic-ref", "--short", "HEAD");
60 if (branch) return `🌿 ${branch}`;
61 const tag = await git("describe", "--exact-match", "--tags", "HEAD");
62 if (tag) return `🏷️ ${tag}`;
63 const sha = await git("rev-parse", "--short", "HEAD");
64 return sha ? `📌 ${sha}` : "";
65}
66
67export async function refresh($: EngineInterface) {
68 const [cwd, model, usage, now] = await Promise.all([
69 $.session.cwd(),
70 $.session.model(),
71 $.session.usage(),
72 $.clock.now(),
73 ]);
74
75 const rateLimit = (kind: string, label: string) => {
76 const limit = usage.rateLimits.find((l) => l.kind === kind);
77 if (!limit) return "";
78 let text = `${label} ${Math.round(limit.percentUsed)}%`;
79 if (limit.resetsAt) {
80 const secs = Math.floor((Date.parse(limit.resetsAt) - now) / 1000);
81 text += ` 🔄 ${formatRemaining(secs)}`;
82 }
83 return text;
84 };
85
86 hud = {
87 model: model ? modelName(model) : "",
88 context: usage.context.percent,
89 fiveHour: rateLimit("five_hour", "🕐 5h"),
90 sevenDay: rateLimit("seven_day", "📅 7d"),
91 cwd,
92 ref: await gitRef($, cwd),
93 };
94 $.ui.invalidate("ui.render");
95}
96
97// The engine's own hint line stays as it is; the HUD adds two rows beneath it.
98export function renderHud(
99 $: EngineInterface,
100 e: RenderInput<"PromptHint">,
101 engine: RenderElement,
102) {
103 if (!hud) return engine;
104 const { Box, Text } = $.ui.resolve(e);
105
106 const claudeParts: [string, string][] = [
107 [COLORS.model, hud.model && `🤖 ${hud.model}`],
108 [
109 COLORS.context,
110 hud.context === undefined ? "" : `🧠 ${Math.round(hud.context)}%`,
111 ],
112 [COLORS.limits, hud.fiveHour],
113 [COLORS.limits, hud.sevenDay],
114 ];
115 const claudeLine = claudeParts.filter(([, text]) => text);
116
117 return (
118 <Box flexDirection="column">
119 {engine}
120 <Text wrap="truncate-end">
121 {claudeLine.map(([color, text], i) => (
122 <Text key={text}>
123 {i > 0 && (
124 <Text color={COLORS.separator}>{" • "}</Text>
125 )}
126 <Text color={color}>{text}</Text>
127 </Text>
128 ))}
129 </Text>
130 <Text wrap="truncate-end">
131 <Text color={COLORS.cwd}>{`📁 ${hud.cwd}`}</Text>
132 {hud.ref && <Text color={COLORS.git}>{` ${hud.ref}`}</Text>}
133 </Text>
134 </Box>
135 );
136}
137