A C port of claude-hud, the Claude Code status line by Jarrod Watts. Same output, byte for byte, without node, in a few milliseconds per refresh.
Everything you see is claude-hud's: its display items, its two layouts, its options and its config file. This project only reimplements it as one small native executable. For what the status line shows and what each option does, read claude-hud's README.
With the default config, inside a git repository with uncommitted changes:
[Opus] │ my-project git:(main*)
Context █████░░░░░ 45% │ Usage ███░░░░░░░ 25% (resets in 1h 31m)
The same as claude-hud prints, except that the last line also ends with the refresh's run time in green, such as
6ms, or under a millisecond in microseconds, such as 412µs.
claude-hud-c is an independent project, not affiliated with Anthropic or with claude-hud's author.
- No node. claude-hud runs on node; on a machine that has no other use for node, installing it just for the status line is a heavy dependency. claude-hud-c is one executable with no runtime to install.
- Faster refreshes. Claude Code reruns the status line command whenever the conversation updates. claude-hud starts node and loads its modules every time; claude-hud-c starts in a few milliseconds. Both read only what was appended to the session transcript since the previous refresh, claude-hud since 0.12.0.
One refresh, median (p50) and 90th percentile in ms:
| Transcript | claude-hud-c, started directly | claude-hud-c, through sh | claude-hud, through sh |
|---|---|---|---|
| 0.3 MB | 7.0 / 13.6 | 34.1 / 48.8 | 133.7 / 139.5 |
| 10 MB | 9.5 / 21.7 | 33.8 / 50.3 | 198.0 / 208.9 |
"Through sh" is how Claude Code runs the command on Windows when Git Bash is set up; about 25 ms of it is sh itself. claude-hud's time grows with the transcript, because 0.10.0 parsed the whole file on every refresh; claude-hud-c's barely moves. claude-hud 0.12.0 reads only what was appended too, so its 10 MB figure would come closer to its 0.3 MB one.
Measured on Windows, claude-hud 0.10.0 on node 24.21.0, Git Bash's sh, git status display off;
40 rounds with the three forms alternated, CPU load 8% before and after; the two sh outputs were identical apart from
the run time claude-hud-c appends. To reproduce: test/compare/Measure-HudLatency.ps1 (Windows).
With git status display on, each refresh also waits for one git process, on both sides alike.
Byte-identical output is the rule every change is held to, not an aim. It is checked at two levels:
- Every build runs the tests under
test/cases, grouped by area (UTF-8, the config files, the fields read from stdin and the transcript, input limits, malformed input, layout, display items, written files, git and jj), comparing claude-hud-c's status line and the files it writes, byte for byte, with what claude-hud printed and wrote for the same input at a frozen clock. claude-hud's output is recorded in the cases, so the tests need only CMake. Another group grows transcripts step by step and holds the incremental parsing to a full parse.ctestruns the same tests. - The comparison scripts under
test/comparerun claude-hud itself next to claude-hud-c: all the session transcripts on the machine crossed with sets of stdin, config, width and time, and generated hostile transcripts and configs. Running them needs node 24 or later and a claude-hud source tree of the version named intest/compare/common.mjs, with itsdist/built; each script's header says how to call it.
Known gaps, where claude-hud-c renders English or an approximation instead:
- claude-hud's
languageoption: only English is implemented, whatever it is set to. This is deliberate: the status line carries only a handful of words, too few to justify the extra machinery. Anyone who wants another language is welcome to add it; the strings sit in one table insrc/render.c, and the output has to match claude-hud's own translation; --extra-cmd(Claude Code'sstatusLinecannot pass arguments anyway);- a few inputs where claude-hud depends on node's locale data or throws, such as a system locale other than en-US or
zh-CN, or a Windows path that is not drive-absolute. The source marks each one
UNSUPPORTED("<Reason>"); - inputs that only crafted data or rare setups produce, such as times before 1970, more than 256 skills, or JSON keys repeated within an object, where matching upstream would take more code than the case is worth.
Deliberate differences: changed files are listed in the order git reports them, unsorted, where claude-hud sorts them
newest first by their times (since 0.15.0 its permission model keeps it from reading those times, so its output keeps
git's order too). claude-hud runs its logic under that permission model, which reads only the files it names up front,
and a rules directory holding a link to a file elsewhere is no longer counted there; claude-hud-c counts it. A config
file that is a symbolic link is read, where claude-hud on Linux and macOS refuses to open it and reports it as
unreadable. Nor does claude-hud-c write claude-hud's heartbeat.json, which only claude-hud's /hud command reads.
Two additions: the run time of each refresh at the end of the last line, and a parse-state file per transcript (next section).
To read only what was appended since the previous refresh, claude-hud-c saves its parse state per transcript, as
claude-hud-c/<TranscriptName>.bin under the temporary directory: %TEMP% on Windows; on Linux and macOS the first
of TMPDIR, TMP, TEMP that is set, else /tmp. A file holds only what claude-hud still tracks, not every tool
the session ever ran; a long session's file is a few hundred KB.
This is claude-hud-c's own design, and the reason its time in the table above stays flat as a session grows: parse only
what was appended, keep the parser's state between refreshes, and trust a saved state only for the same file (by its
identity) whose first block and the block before the saved offset still hash the same. claude-hud 0.12.0 added
incremental reading of its own, with its saved states under its plugin directory (transcript-cursors); claude-hud-c
neither reads nor writes those.
A state is saved again once what was appended past it reaches a quarter of its size, or at once after a line that takes the refresh's clock (a tool line without a time); until then each refresh parses those lines again, which costs less than writing the state.
The files are a cache: deleting them is always safe, and a missing or outdated one only costs a full parse of that transcript on its next refresh, after which the state is saved again. A new build of claude-hud-c ignores the files an earlier build wrote and replaces them as it goes.
- Windows: the temporary directory survives reboots, and claude-hud-c never deletes its files, so the directory
grows by one file per transcript. Delete
%TEMP%\claude-hud-cwhenever you like. - Linux and macOS: the temporary directory is usually emptied at boot (
/tmpon tmpfs,systemd-tmpfiles) or periodically (macOS), so the first refresh of each session after that parses its transcript in full. Nothing else changes.
Windows, Linux and macOS, each on x64 and arm64. CI builds all six and runs the tests on each, on a machine of that architecture.
- Windows: the primary platform, Windows 10 or later. No C runtime is linked; the executable imports from
KERNEL32.dllonly. - Linux: the executable is linked statically: it runs whatever glibc the machine has, and no refresh spends time loading the C library.
- macOS: 11 or later. Tested in CI only; no one has used it day to day on a Mac yet.
Prebuilt executables are on the Releases page, one archive per
platform: claude-hud-c-<Os>-<Arch>.zip on Windows, .tar.gz on Linux and macOS. Each holds the executable alone,
claude-hud-c.exe on Windows and claude-hud-c elsewhere, already executable. SHA256SUMS beside them lists the
archives' checksums. The latest release is always at
https://github.com/yx64/claude-hud-c/releases/latest/download/<Archive>. On macOS an archive downloaded through a
browser is quarantined, and so is the executable Finder unpacks from it; xattr -d com.apple.quarantine <Executable>
lifts that.
To build from source you need CMake 3.28 or later, Ninja, and clang with C23 support and lld (clang 18 is too old). On
Linux the executable links glibc statically, which takes its static archives (libc6-dev on Debian and Ubuntu,
glibc-static on Fedora).
Neither node nor claude-hud is needed to build, test or run it.
git clone https://github.com/yx64/claude-hud-c
cmake -S claude-hud-c -B <BuildDir> -G Ninja -DCMAKE_C_COMPILER=<Clang>
cmake --build <BuildDir>
The build runs the tests as well and fails when one fails. Besides Ninja's progress line for each test group,
it prints one summary line, such as tests: 427/427 passed, or a FAIL line per failed check with where the
output differs. cmake --build <BuildDir> -- --quiet drops Ninja's progress lines (Ninja 1.11 or later), so a
passing build prints that summary line alone. To build without the tests, configure with -DHUD_TEST_AFTER_BUILD=OFF;
ctest --test-dir <BuildDir> --progress --output-on-failure --no-tests=error runs them at any time.
Copy the executable, unpacked or built (<BuildDir>/release/claude-hud-c.exe, claude-hud-c off Windows), to a
directory that is not on PATH, such as ~/.local/tools/, and point statusLine in the user-level
~/.claude/settings.json at it:
"statusLine": { "type": "command", "command": "~/.local/tools/claude-hud-c.exe" }Use forward slashes: Claude Code runs the command through a shell, and Git Bash drops unquoted backslashes. That shell
also expands the ~. To update, rename the old executable aside before copying or unpacking the new one into its place,
since a refresh may be running it; on Windows, writing over it then fails.
The config is claude-hud's, read from the same place, ~/.claude/plugins/claude-hud/config.json (under
CLAUDE_CONFIG_DIR when that is set). An existing claude-hud config works as it is. To go back to claude-hud,
install the plugin and run /claude-hud:setup, which points statusLine at it again.
claude-hud-c follows claude-hud; it is not a fork with a direction of its own, and it is not meant to replace it.
- No new features. Feature requests belong in claude-hud. When claude-hud adds something, claude-hud-c may follow that release.
- Performance work is not a feature. It changes how fast the output comes, never what it is. The parse-state cache (see "Parse-state files") is one such change, designed here rather than taken from claude-hud.
- Bug reports are welcome: output that differs from claude-hud for the same input, crashes, wrong files written.
- Performance ideas are welcome too, from anyone who enjoys shaving off milliseconds, on one condition: no
functionality is lost. A change has to keep the output byte-identical to claude-hud, which the comparison tests under
test/check. - If claude-hud stops adding features, claude-hud-c will not add any on its own. Should Claude Code ever ship a HUD of its own, that is outside this project.
A bug is output that differs from what claude-hud prints for the same input, so the input is what makes a report useful:
- the JSON Claude Code passed on stdin, and your
config.json; - what claude-hud-c printed, and what claude-hud prints for the same stdin, if you can run it;
- your platform, and the claude-hud-c commit you built.
Check them before attaching: the stdin holds paths and session ids, and a session transcript holds your whole conversation, so do not attach one unless asked, and then cut it down to the lines that matter.
- claude-hud by Jarrod Watts, MIT licensed: the display, the behaviour and the config format all come from it. The incremental transcript parsing is claude-hud-c's own.
- yyjson for JSON and utf8proc for
Unicode, both included unmodified under
vendor/with their licenses.
MIT, see LICENSE. Being a port of claude-hud, it keeps claude-hud's copyright notice next to its own. The
vendored libraries carry their own licenses next to them in vendor/.