A tiny status-line indicator for Claude Code that shows — at a glance, at the bottom of your screen — whether the headroom context-compression MCP is actually being used in your current session, and how many tokens it has saved you.
No more wondering "did I remember to compress that huge file, or did I just burn context?" — the indicator tells you, honestly, in real time.
Type these into the Claude Code prompt:
/plugin marketplace add Abhi902/headroom-plugin
/plugin install headroom-usage-indicator@headroom-tools
Then ask Claude, in plain English: "run the headroom doctor" (or invoke it directly: /headroom-usage-indicator:doctor). It checks everything — jq, the headroom compression engine, the MCP server, the hooks, the status line — and, with your consent, fixes whatever is missing, including the one thing a plugin can't do by itself: writing the statusLine entry into your ~/.claude/settings.json.
That's it. As of v2.5 the plugin is self-contained: the hooks (Dangi and the hcat gate) register themselves the moment the plugin is enabled, hcat is already on Claude's Bash PATH, and the headroom MCP server registration comes bundled — no scripts to copy, no settings.json hook surgery. If you installed an earlier version by hand, see Migrating from a pre-v2.5 install.
Paste this into the Claude Code prompt:
Create a roughly 100 KB JSON file at /tmp/hr-demo.json (an array of a few thousand small objects), then show me what's in it.
You should see, in order:
- the hcat gate step in — instead of raw-reading the file, Claude gets redirected to run
hcat /tmp/hr-demo.json; - the output open with a receipt line like
── hcat: /tmp/hr-demo.json · 1 lines · 98.3 KB · ~25000 tok → ~7500 tok (70.0% saved) · original on disk …; - the badge at the bottom of your screen flip green within a second or two:
● headroom · ~17.5k tok · $… · 1× ….
If any of those three don't happen, ask Claude to run the headroom doctor — diagnosing exactly this is its whole job.
The badge across its three real states — red idle (big files read raw), green active (an hcat receipt just landed: tokens and $ saved), grey decayed (quiet 60s, keeps the tally and flags what was missed).
This plugin does not compress anything by itself — the actual compression is done by headroom, a local Python engine (→ https://github.com/headroomlabs-ai/headroom). Think of it like a fuel gauge: headroom is the engine, this plugin is the gauge (plus, since v2.3, a hand that reaches for the fuel-saver button for you).
The good news: you no longer have to plumb the engine in yourself. The plugin bundles the MCP server registration (its tools appear as mcp__headroom__headroom_compress and friends), and if the engine itself is missing, the doctor offers to bootstrap it into ~/.headroom-venv. If headroom is absent and you decline, everything stays politely silent — the badge sits at "idle", the gate lets Reads through — nothing breaks.
v2.7.3 — the bundled MCP server actually connects now. From v2.5 through v2.7.2 the bundled registration was shipped broken: its launcher path carried literal quotes, and because Claude Code spawns an MCP
stdiocommand directly (no shell ever unwraps quoting), every connection attempt died instantly and/pluginshowed a ✗ beside headroom. It went unnoticed because anyone with a pre-plugin manual registration in~/.claude.jsonstill had the tools from there. If you're upgrading, run/headroom-usage-indicator:doctor --fixonce — it repairs an already-installed broken copy in place (with a backup), and from now on it judges that launcher exactly as spawned rather than being fooled by its own quote-stripping.
Every second, it looks at what your Claude session has actually done and updates a small badge in your status line. It reads the real session activity (not guesses), so it can't be fooled — just looking at headroom's stats does not make it say "active"; only a real compression does.
| Badge | Colour | Meaning |
|---|---|---|
○ headroom idle (not compressing yet) — or ○ headroom idle · 4 big blobs uncompressed |
🔴 red | headroom hasn't compressed anything yet this session; the count appears when large tool outputs are going uncompressed |
● headroom · ~2.4k tok · $0.01 · 3× | $1.83 all-time 😴 dangi |
🟢 green | a compression just happened — tokens saved, money saved, how many times, and your all-time total |
○ headroom idle · ~2.4k tok · $0.01 · 3× · 2 missed | $1.83 all-time 🤖 dangi: 2! |
⚪ grey | quiet for 60s — dims, but keeps the totals; · N missed counts big results beyond what you've compressed |
▲ headroom broken (engine) · run /doctor |
🟡 yellow | a hook or hcat just recorded a real engine failure — takes over the badge until a clean /doctor run (or the next successful hcat) clears it (v2.7) |
- The token count is the running total for the whole session (it adds up every compression).
- It resets to red when you start a brand-new Claude session.
- v2.7: the badge is no longer just idle/active — it also notices when it's broken. See Ambient health below.
The badge prices the tokens headroom saved at the input rate of the model your session is running (e.g. $5/MTok on Opus, $10/MTok on Fable). The price table is data, not code (v2.6): it lives in data/model-prices.json (matched by model-id substring, first match wins), so adding a model is a one-line edit shipped with the plugin — no script change. If the model isn't in the table, the badge just shows tokens — it never guesses a dollar figure. all-time is the sum across all your sessions on this machine (stored in ~/.claude/headroom-indicator/).
This is a deliberately conservative floor: compressed content would otherwise re-enter the context on every later API turn (mostly at the cheaper cache-read rate), so the true savings compound above the number shown.
Any tool result of 4 KB or more that wasn't produced by headroom itself. Each compression you run forgives one big blob (compressing doesn't remove the original from the transcript, so a plain count would nag you about blobs you already handled). It's a size-only heuristic — a big code file you're editing may be a deliberate non-compression; treat the number as a nudge, not an accusation.
Dangi is the plugin's real-time detector. The badge tells you what you missed; Dangi catches it as it happens:
- the moment a tool spits out ≥ 4 KB that isn't compressed, Dangi whispers to Claude (an in-context nudge, max once a minute) so it can compress right away — and when it can tell which file the output came from (a
Read'sfile_path, or acat/headof a.json/.csv/.log/…in Bash), the nudge names that file so the fix is copy-paste (hcat "<that file>"), v2.6, now file-aware on theReadpath too (v2.7); - v2.7 — true size, not payload size: the platform truncates the hook's own payload to ~10K chars before Dangi ever sees it, so for whole-file ingests — a bare
cat/hcatof the file, or aReadwith no offset/limit — Dangi nowstats the file on disk and reports/tiers onmax(payload size, file size)instead of the truncated figure. A file merely named in a filter command (grep ERROR big.log) or read bounded keeps the payload size — that output really was small. The trigger still fires on payload size only — a file that's written but never read still doesn't nudge; - v2.7 — size-tiered advice: below
DANGI_HUGE_BYTES(default 131072 bytes / 128 KiB) the nudge is the usual hcat/MCP-compress suggestion; at or above it, Dangi advises delegation instead of compression — spawn a disposable subagent to read/analyze the file and return only conclusions or an hcat-compressed digest, since compressing that much content in place would still flood the window; - because the nudge is rate-limited, blobs that slip by while Dangi is quiet aren't lost — the next nudge says how many were missed in the gap, so batching never hides the backlog (v2.6);
- v2.7 — it remembers: a nudge on a file-backed blob ≥ 4 KB that actually looks structured (innate extension, or the same 512-byte sniff the gate uses — a big source file can't get itself compression-gated) also records that file as a learned "offender" (see Detection that learns below), so the hcat gate can catch it again later even off the static extension list;
- if it keeps happening, you get a desktop notification — via
osascripton macOS, falling back tonotify-sendon Linux (max once per 5 minutes); - and he lives at the end of your status line:
😴 dangiwhen all is well,🤖 dangi: 3!when compression chances are slipping by.
Dangi ships as a plugin hook — registered automatically while the plugin is enabled, gone when it isn't. Set DANGI_NO_NOTIFY=1 to silence the notifications. Dangi knows what not to nag about: edit tools (Edit/Write/MultiEdit/NotebookEdit echo the code you're changing), web results (WebFetch/WebSearch return prose), image-bearing outputs (base64, not text-compressible), headroom's own tools, and the output of a genuine hcat run (that is the compression).
The badge and Dangi are honest, but they share a limit: by the time Claude could call headroom_compress, the big output is already in context — those tokens are spent, and re-sending the blob to the compressor costs output tokens on top. headroom_compress genuinely pays off inside subagents (compress before returning), but in the main session it's mostly consolation.
v2.3 adds the prevention layer:
hcat <file>(shipped in the plugin'sbin/, on Claude's Bash PATH while the plugin is enabled) compresses a structured file through headroom's local pipeline before it ever enters context — you get a compact schema+rows rendering (typically 70 %+ token reduction on JSON) plus a header citing the original path. Need an exact detail later?Readthe original with an offset/limit — the file on disk is the source of truth. Savings are reported intoheadroom_stats.- The hcat gate (a plugin PreToolUse hook) catches Claude about to raw-read a big (≥ 16 KB) structured file. For a
Read, it still denies once per file per session with the exacthcatcommand to run instead; re-Reading the same file passes, so it's a redirect, never a wall. For a bare, single-linecat <file>in Bash, v2.7 rewrites the command in place (cat <file>→hcat "<file>", viaupdatedInput, one shot) instead of denying it — the hcat receipt in the output makes the substitution visible, anadditionalContextline tells Claude its command was rewritten, and there's no deny→re-plan→retry round trip. (Multiline commands are never touched — rewriting one line would silently drop the others.) SetHCAT_GATE_NO_REWRITE=1to restore the old deny-and-suggest behavior for Bash. If headroom isn't installed the gate stays silent. Kill switch:HCAT_GATE_OFF=1.
Both ship with the plugin — there is nothing to copy or register.
v2.4: the badge finally sees hcat. Every hcat run leaves a receipt in the transcript (── hcat: … ~18899 tok → ~9351 tok (50.5% saved)); the status line now parses those receipts and folds them into the token count, the dollar figure, the N× counter, the freshness dot, and the all-time total — passthrough receipts (files hcat couldn't shrink) count as nothing, and a big genuine receipt is never a "missed" blob (it is the compression). Before v2.4 the badge only counted headroom_compress MCP calls, so a session that saved everything via hcat still read "idle (not compressing yet)". v2.4.1: Dangi recognizes receipts too. v2.5: receipt attribution is structural on both paths — a receipt only counts when the tool result actually came from a Bash command that invoked hcat; an output that merely quotes a receipt line (a grep over docs, a cat of this README) counts as nothing — and, if big, as a missed opportunity.
v2.7 — hcat works even without the Python engine. When no engine is found, hcat no longer just refuses: it renders a lossless TOON-lite table via pure jq — a uniform JSON array of same-shaped objects becomes one header row plus CSV-like rows, typically 30–60% smaller, zero extra dependencies — before giving up. The receipt reads … (NN.N% saved · toon-lite lossless, engine absent) … and the token figures are ~4-bytes/token estimates (there's no engine tokenizer to ask). Files that aren't a uniform array of flat objects still exit 3 with the old "headroom python not found" message. When the engine is installed but its semantic compressor would save less than 5% on a given file, the same TOON-lite reformat is tried in Python before falling back to raw passthrough — receipt … (NN.N% saved · toon-lite lossless) …, reported into headroom_stats with strategy:"toon-lite". Either way it's only used when it actually saves ≥ 5%; otherwise passthrough stands as before.
The hcat gate's static extension list (.json/.jsonl/.ndjson/.csv/.tsv/.log) misses extensionless API dumps and mislabeled .txt JSON. Two things close that gap without widening the list:
- Offender memory — every time Dangi nudges on a file-backed blob whose file is ≥ 4 KB, it appends
<epoch> <path>to$STATE_DIR/offenders(deduped by path, entries older thanHEADROOM_OFFENDER_TTLseconds — default1209600, 14 days — pruned on every write). The hcat gate treats a fresh offender entry as gate-eligible regardless of extension, so a file that burned context once gets caught on its nextRead/cat. The list is plain text — inspect or delete$STATE_DIR/offendersto see or reset what's been learned. - Structural sniff — failing both the extension list and the offender list, the gate reads the first 512 bytes of an eligible-sized file: if the first non-space character is
{/[, or the first two lines carry the same ≥3 comma/tab count, it's treated as structured anyway. Disable withHCAT_GATE_NO_SNIFF=1.
Every layer above fails silently by design — a hook that prints anything but its one JSON decision breaks every tool call. That's normally fine (headroom missing = the gate stays quiet, hcat missing = passthrough), but it means a real breakage — a half-created venv, a broken HCAT_PYTHON, a compression that raised — used to look exactly like ordinary idle. v2.7 makes real failures visible:
- Hooks and
hcatrecord genuine engine failures (not "never installed" — "resolved and then broken") as<epoch> <component> <message>in$STATE_DIR/last-error. - A fresh entry (under 24h old) takes over the badge:
▲ headroom broken (<component>) · run /doctor, in yellow, replacing the usual active/idle state. hcatclears its ownengine/runtimeerrors the instant a compression actually succeeds;/doctorclears the file on a fully clean run and reports "cleared recorded failure state — badge restored".- A new SessionStart hook,
scripts/session-probe.sh, runs a fast subset of the doctor's checks once per session —jqpresent,hcatexecutable, engine python resolvable (existence only; the import itself is checked at use time by the gate/hcat), the bundled price table parses — and stays silent when everything's fine. A genuinely never-installed engine still gets a friendly one-line pointer to/doctor --fix; that alone does not flip the badge to broken (it's the ordinary idle state, not a breakage). - v2.7.2 — setup nudge: when everything else is healthy but the status line isn't wired yet (the one manual step — a plugin can't register a status line itself), the probe emits a one-line
🤖 headroom setup: … run /headroom-usage-indicator:doctor --fix to show it. This is why a fresh install no longer leaves you wondering where the badge is — it tells you the remaining step. It's a reminder, not a breakage (nolast-error, no yellow badge), it defers to any real problem, and it goes silent the moment the status line is wired. The same nudge fires if the status line is wired but itslib/deps are missing (the #2 shape — badge would read zero).
A Stop/SessionEnd hook, scripts/ledger-hook.sh, walks the transcript with the same structural attribution as the badge and appends one cumulative snapshot per session to $STATE_DIR/ledger.jsonl: tokens/dollars saved, plus the big outputs that went uncompressed — count, size, an estimated token cost, and the biggest offenders' paths. A per-session checksum marker keeps a busy session's many Stop firings from spamming the file, and a session that neither saved nor missed anything writes nothing.
At the next session's start, session-probe.sh surfaces the last ledger line once (never twice, even across many sessions) as an invoice:
🤖 headroom invoice: last session: saved
12.4k tok ($0.06) · 3 big output(s) went uncompressed (~8.2k tok ≈ $0.04 left on the table — biggest: /tmp/report.json)
For the curious — after the Quickstart, here is where everything lives:
| Piece | Where | How it got there |
|---|---|---|
| Dangi, the hcat gate, session-probe, the session ledger | hooks/hooks.json inside the plugin |
auto-registered while the plugin is enabled (SessionStart, PreToolUse, PostToolUse, Stop, SessionEnd) |
hcat |
bin/hcat inside the plugin |
on Claude's Bash PATH automatically |
| headroom MCP registration | .mcp.json inside the plugin |
bundled; the launcher finds your engine |
| headroom engine (Python) | ~/.headroom-venv (or your own install) |
the doctor bootstraps it with your consent |
| status line | statusLine in ~/.claude/settings.json, pointing at a copy of scripts/statusline.sh at ~/.claude/headroom-statusline.sh (with its attribution.jq + headroom-state.sh deps in ~/.claude/lib/) |
the one manual step — the doctor writes it for you (merge-aware: an existing custom status line is kept and backed up under _headroomStatusLineBackup), and provisions the lib/ deps the badge needs to count savings (v2.7.1) |
If you'd rather wire the status line by hand, the merge-aware installer lives in skills/headroom-usage-indicator/SKILL.md; the standalone entry it writes boils down to (with your real home directory in place of /Users/you — a ~ inside the quoted path would never be expanded):
"statusLine": { "type": "command", "command": "bash \"/Users/you/.claude/headroom-statusline.sh\"", "refreshInterval": 1 }If the badge doesn't appear at the bottom right away, type /statusline once to refresh — or it'll be there next session.
If you hand-wired this before v2.7.3, check your quoting. Earlier versions of this README showed the path as
bash "~/.claude/headroom-statusline.sh"— but no shell expands a~inside quotes, so that entry could never resolve and the badge silently never rendered. v2.7.3's doctor detects that (in either quote style), reports it, and--fixrewrites the entry to an absolute path. An unquotedbash ~/.claude/...was always fine and is left alone.
New versions arrive through the plugin marketplace:
/plugin marketplace update headroom-tools
/plugin update headroom-usage-indicator@headroom-tools
The hooks, hcat, and the MCP definition update with the plugin — nothing to re-copy. The one exception is the status-line script, which runs from a copy at ~/.claude/headroom-statusline.sh (plus its ~/.claude/lib/ deps): if a release changes it, ask Claude to run the doctor once and it refreshes the copy and the deps. Coming from v2.7.0 or earlier, run /headroom-usage-indicator:doctor --fix once after updating — earlier installs never provisioned the badge's lib/ deps, so it was stuck reporting zero savings until you do (#2, fixed in v2.7.1). Legacy (pre-v2.5) manual installs get none of this for free — every update means re-running the installer, which is one more reason to migrate.
Coming from v2.7.2 or earlier, run /headroom-usage-indicator:doctor --fix once too — v2.7.3 fixes the bundled MCP registration that never connected (see above), and --fix repairs the already-installed broken copy rather than waiting for the next fresh install.
Leaving should be as easy as arriving:
/plugin uninstall headroom-usage-indicator@headroom-tools
That removes the hooks, hcat, and the MCP registration in one go. Then tidy the two things that live outside the plugin:
- remove the
"statusLine"block from~/.claude/settings.json(or ask Claude to "remove the headroom status line" — if you had a custom status line before, restore it from_headroomStatusLineBackup); - delete the state and the script copy:
rm -f ~/.claude/headroom-statusline.sh
rm -rf ~/.claude/headroom-indicatorIf you ever did a pre-v2.5 manual install, also remove the old copies and their settings.json hook entries — see the migration note below. The headroom engine itself (~/.headroom-venv, if the doctor created it) is yours to keep or rm -rf as you please.
Before v2.5, the installer copied scripts into ~/.claude/ and registered hooks directly in your settings.json. If those leftovers are still present alongside the plugin, the hooks double-fire (two Dangis, both polite, still one too many). Ask the doctor to clean up — with your consent it removes:
- the
hooks.PostToolUseentry referencingdangi-hook.shand thehooks.PreToolUseentry referencinghcat-gate.shfrom~/.claude/settings.json; - the copies
~/.claude/dangi-hook.sh,~/.claude/hcat-gate.sh, and~/.claude/hcat.
The status-line copy (~/.claude/headroom-statusline.sh) stays — that one is still how the badge runs.
It always says "idle" — why?
Most likely the headroom engine isn't installed or the MCP isn't loading. Ask Claude to run the headroom doctor — it checks each link in the chain and tells you which one is broken. (Manual check: mcp__headroom__headroom_compress should exist in your session's tools.)
If /plugin shows a ✗ beside headroom and those tools are missing entirely, you're on a version between v2.5 and v2.7.2, where the bundled MCP registration could never connect. Update to v2.7.3 and run /headroom-usage-indicator:doctor --fix — see The gauge and the engine.
If the engine is working and hcat is clearly compressing (you see receipts in the transcript) but the badge still sits at idle showing zero, the status-line script is missing its runtime deps. The badge runs from a copy at ~/.claude/headroom-statusline.sh and reads attribution.jq + headroom-state.sh from ~/.claude/lib/ next to it; without them it silently degrades to zero. Run /headroom-usage-indicator:doctor --fix — it (re)installs those deps and the badge starts reporting real totals. (Fixed in v2.7.1 — earlier installs never provisioned them; #2.)
If the badge says "N big blobs uncompressed" for files the gate did compress — you saw the cat get rewritten and the receipt land, and it still reads zero — that's fixed in v2.7.4. The gate rewrites cat into an hcat run, but Claude Code records the original cat in the transcript, and the badge attributed receipts by reading that recorded command. So every auto-rewritten compression banked nothing and was then counted as a miss — the badge reported the exact opposite of what happened, and the session ledger and next-session invoice inherited it. Update and the numbers correct themselves; nothing to re-run. (Same release: a large passthrough receipt — incompressible content, where the raw bytes really did enter the window — now counts as a miss instead of being silently exempt.)
It says "▲ headroom broken" — what now?
That's different from idle: a hook or hcat recorded a real engine failure in the last 24h (not "never installed" — "resolved and then broken", e.g. a bad HCAT_PYTHON or a half-created venv). Run /doctor — it explains the specific failure and, once the underlying issue is fixed, clears the recorded state and restores the badge.
Do I still have to remember to compress things? Less than you used to. The hcat gate redirects big structured-file reads automatically, and Dangi nudges Claude about the rest. The badge is the honest scorekeeper on top.
I already have a custom status line — will this wipe it?
No. The status-line setup is merge-aware: it appends the headroom badge to your existing status line (so you keep Model · ctx · dir (branch) and gain the headroom dot) and backs up your original under _headroomStatusLineBackup in settings.json. To restore, copy that key back over statusLine.
Can I change the colours / the 60-second decay / show a different tool?
Yes — see the Customize section in skills/headroom-usage-indicator/SKILL.md. The same pattern works for any MCP tool (mcp__server__tool), not just headroom.
Is any of this sent anywhere?
No. It's a local shell command reading your local session file. The engine runs offline (HF_HUB_OFFLINE=1, update checks off). Nothing leaves your machine.
If you can't (or won't) use the plugin marketplace, the copy-everything-to-~/.claude flow still works. Clone this repo, then follow the legacy fallback installer at the bottom of skills/headroom-usage-indicator/SKILL.md — it copies statusline.sh, dangi-hook.sh, hcat-gate.sh, hcat, and (v2.7) session-probe.sh and ledger-hook.sh into ~/.claude/, and registers all five hook events (SessionStart, PreToolUse, PostToolUse, Stop, SessionEnd) in your settings.json itself — the plugin's hooks/hooks.json does this automatically, so a legacy install has to do it by hand.
Two honest caveats about the legacy flow:
hcatis NOT on Claude's PATH in a legacy install — the "on PATH" convenience only exists while the plugin is enabled. Claude must invoke it by full path:~/.claude/hcat <file>. The gate is install-aware: in a legacy layout its deny message cites that full sibling path (~/.claude/hcat "<path>"), and the bare-hcat/on-PATH wording appears only for plugin installs.- You must also install and register the headroom engine and MCP server yourself (→ https://github.com/headroomlabs-ai/headroom), and you need
jq(brew install jqorapt install jq).
Do not run the legacy installer if the plugin is installed — you'd register every hook twice.
skills/headroom-usage-indicator/SKILL.md— the status-line skill: the merge-aware installer, how the badge works, a common-mistakes table, verification steps, and customization notes.skills/doctor/SKILL.md— the doctor: checksjq, the engine, the MCP, the hooks, the status line, and ambient-health state; fixes what you consent to, including legacy-install and project-settings cleanup.hooks/hooks.json— plugin-native registration for session-probe (SessionStart), the hcat gate (PreToolUse), Dangi (PostToolUse), and the session ledger (Stop, SessionEnd).bin/hcat— compress-at-the-source, on Claude's PATH while the plugin is enabled; falls back to a lossless TOON-lite (jq-only) rendering when the Python engine is absent.scripts/—statusline.sh,dangi-hook.sh,hcat-gate.sh,session-probe.sh,ledger-hook.sh,doctor.sh,mcp-launcher.sh(the working parts), plusscripts/lib/(attribution.jq,headroom-state.sh— shared deps the installer copies next to the status-line script).data/model-prices.json— the badge's price table as data; adding a model is an edit here, not a code change..mcp.json— bundled headroom MCP server definition (the launcher finds your engine).test.sh— the synthetic-transcript test suite; run it from the repo root.
MIT
