cd /news/developer-tools/show-hn-cc-context-telemetry-context… · home topics developer-tools article
[ARTICLE · art-63316] src=github.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Show HN: cc-context-telemetry - context and rate-limit % in Claude Code's bar

Developer alagiz released cc-context-telemetry, an open-source tool that displays Claude Code's context usage percentage, Pro/Max rate-limit status with reset countdowns, and the active model directly in the terminal statusline bar. The tool wraps any existing statusline command and requires no Node.js runtime for native installations, addressing a gap where Claude Code's own hooks and plugins cannot surface these metrics.

read12 min views1 publishedJul 17, 2026
Show HN: cc-context-telemetry - context and rate-limit % in Claude Code's bar
Image: source

Show Claude Code's context % and Pro/Max rate-limit usage (with how long until each limit resets) and the current model, right next to your existing statusline bar.

before:   ~/code/myapp  main

after:    ctx 48% | 5h 14% ~3h20m | 7d 50% ~4d | opus-4.8   ~/code/myapp  main

The trailing part is whatever your own statusline already prints - cct wraps ANY statusline command, not a specific one (a busier bar, like the adtention plugin's main $9.07 21:04

, gets the same segment prepended).

Install:

npm i -g cc-context-telemetry

(No Node or npm on the machine, e.g. a native Claude Code install? Skip the npm step: git clone

the repo and use the absolute path to its bin/cct-statusline

as the command

below. The statusline path is pure POSIX shell, so it needs neither Node nor npm.)

Set it as your statusLine

in ~/.claude/settings.json

:

{
  "statusLine": { "type": "command", "command": "cc-context-telemetry-statusline" }
}

That is it - your statusline is now ctx % | 5h % | 7d % | model

with reset countdowns. (Settings changes take effect in a new session. Prefer a stable absolute path? Point command

at an installed or checked-out bin/cct-statusline

instead of the global shim.)

Already run a statusline you want to keep? Add CCT_WRAP

, set to your existing statusline command; the segment then prepends to it on the same line:

{
  "statusLine": {
    "type": "command",
    "command": "cc-context-telemetry-statusline",
    "env": { "CCT_WRAP": "<your existing statusline command>" }
  }
}

(If your Claude Code version does not accept an env

block, export CCT_WRAP

in your shell profile instead.)

Either direction chains, because cct exec

s through: cct can wrap your bar (above), or if another statusline tool already owns your statusLine

, point THAT tool at cct instead of the reverse - put cct's command in the host's own wrap channel so it keeps managing the statusLine. See examples/host-owned-statusline.md.

CCT_SEGMENTS

picks which segments show and in what order - a comma (or space) separated list. Default: ctx,5h,7d,model

. Order is the display order; presence toggles a segment on or off; unknown tokens are ignored; unset or empty uses the default. Tokens:

ctx

  • context window used %5h

/7d

  • Pro/Max rate-limit usage, each with a~time-left

reset countdownmodel

  • the current model as a short token (e.g.opus-4.8[1m]

,fable-5

)

CCT_SEGMENTS="ctx,5h,7d"          # drop the model
CCT_SEGMENTS="model,ctx,5h,7d"    # model first
CCT_SEGMENTS="5h,7d"              # just the rate limits

Set it in your statusLine

env block in ~/.claude/settings.json

, next to command

(and CCT_WRAP

if you wrap an existing bar):

{
  "statusLine": {
    "type": "command",
    "command": "cc-context-telemetry-statusline",
    "env": { "CCT_SEGMENTS": "ctx,5h,7d" }
  }
}

(Or export CCT_SEGMENTS

in your shell profile instead.)

Both set an environment variable Claude Code reads at launch, so the change takes effect only in a NEW session - existing sessions keep their current bar until you start a fresh one. If another tool owns your statusLine

(a host-owned setup, e.g. a plugin that runs cct through its own wrap channel), setting the variable in that tool's per-render command takes effect on the next render with no restart; see examples/host-owned-statusline.md.

Remove the statusLine

block from ~/.claude/settings.json

(or set command

back to your original statusline command, i.e. your CCT_WRAP

value), then npm uninstall -g cc-context-telemetry

(or delete your clone). Optionally clear the telemetry files with rm -rf ~/.claude/cc-context-telemetry

.

Claude Code hooks and plugins cannot see context fullness or rate limits. The ONLY place Claude Code exposes the authoritative context_window

(used percentage, window size) and Pro/Max rate_limits

is the statusLine

command. This tiny, zero-dependency library bridges that gap: a statusLine wrapper that prepends a compact segment - context %, the 5h/7d rate limits each with a ~time-left

reset countdown, and the current model (default ctx,5h,7d,model

, all configurable) - to your own bar, and also writes that telemetry to a per-session file your hooks can read.

ctx

is per session. The 5h/7d rate limits are account-wide but Claude Code only refreshes them on an API call, so any single session's view is often stale. The bar therefore shows, per window, the reading from whichever of your open sessions ON THIS MACHINE refreshed most recently ("latest API call wins": each session tracks when its rate_limits last changed, and the newest change is picked). A reading whose reset boundary already elapsed is treated as stale and never picked; if no session has a current reading for a window, the bar falls back to this session's last-known usage with no countdown. So your open sessions agree instead of showing different stale numbers. It does not converge across separate machines.

One job, three pieces:

bin/cct-statusline

  • the POSIX shell entry Claude Code calls as its statusLine, every render. It ispure shell, with NO Node on the per-render path: it reads the payload once, extracts the session id, and atomically writes the RAW payload totelemetry-raw-<session>.json

. Then it prints thectx % | 5h % | 7d % | model

segment (each rate-limit field carrying a~time-left

countdown to its reset) and, ifCCT_WRAP

is set to your original statusline command,so its bar appends right after the segment on the same line and it BECOMES the statusLine process (see "Safe to run every render" for why this matters); with noexec

s that commandCCT_WRAP

the segment is the whole bar, never blank. There is no per-render Node spawn at all (an earlier design spawnednode

every render; across many sessions that piled up).index.js

  • the consumer.readTelemetry(sessionId)

reads the raw payload file, parses it ON DEMAND, and returns the latest reading with a freshness check. So all JSON parsing happens here, only when a hook actually asks - never on the hot path. It also prunes stale per-session raw files (off the hot path, throttled).bin/telemetry.js

  • an on-demand CLI reader overreadTelemetry

, for the manual live check. It is NOT in the per-render path; it runs only when you invoke it.

Zero third-party dependencies, and cross-platform (CI-tested on Linux, macOS, and Windows): the pure-POSIX-shell statusline path runs anywhere Claude Code does. On Windows, Claude Code runs statusLine commands through Git Bash, which ships the shell and tools it needs. Node

= 18 is required only for the hooks API (

readTelemetry

) and thebin/telemetry.js

reader. Never throws, never callsclaude

.

In any hook (PreToolUse, PreCompact, etc.), read the latest reading:

const { readTelemetry } = require('cc-context-telemetry');
const d = require('fs').readFileSync(0, 'utf8'); // hook stdin
const sessionId = JSON.parse(d).session_id;
const t = readTelemetry(sessionId);
if (t && t.fresh && t.contextPct >= 85) { /* near the wall: act */ }

readTelemetry(sessionId, opts)

returns null

when absent, otherwise:

{
  sessionId,        // string
  contextPct,       // number or null (context window used %)
  usedPercentage,   // number or null (alias of contextPct)
  windowSize,       // number or null (context_window_size)
  fiveHourPct,      // number, OMITTED when Claude Code did not report it
  sevenDayPct,      // number, OMITTED when Claude Code did not report it
  fiveHourResetsAt, // number (Unix epoch seconds), OMITTED when absent
  sevenDayResetsAt, // number (Unix epoch seconds), OMITTED when absent
  model,            // string or null (model id)
  ts,               // ISO timestamp string or null
  source,           // "statusline"
  fresh             // true when contextPct is a finite number AND ts is within the TTL
}

contextPct

/ usedPercentage

are passed through verbatim from Claude Code's context_window.used_percentage

(a percentage, 0..100 in practice). The library does NOT range-clamp them; it only verifies they are numbers, so treat them as advisory and do not assume a strict 0..100

bound. fresh

already rejects non-finite values.

fiveHourResetsAt

/ sevenDayResetsAt

are the Unix epoch (seconds) when each rate-limit window next resets. They are omitted (like the percentages) when the payload has no rate_limits

or no resets_at

, and also when the value is not a plausible epoch - the reader rejects non-epoch or absurd values (mirroring the statusline renderer) so a corrupt file cannot hand you a bogus reset time. Compute time-left yourself, e.g. fiveHourResetsAt - Date.now() / 1000

. A reset time can still be in the PAST when the reading is stale (Claude Code refreshes rate_limits

only on an API call), so gate on fresh

and treat an already-passed reset as stale, not as "resetting now".

fresh

rejects both stale and far-future timestamps (it checks the absolute age against the TTL). When fresh

is false, treat it as "no signal" and stay observe-only. opts.ttlSec

(or env CCT_TTL_SEC

) overrides the default 120s TTL. An override is honored only when it is a finite number greater than 0; a blank, zero, negative, or non-numeric value falls back to the default.

The module also exports DEFAULT_TTL_SEC

(the 120s default, a number), parsePayload(d, sessionId)

(the raw-to-normalized field mapping), rawPath(sessionId)

, pruneRaw()

, telemetryDir()

, telemetryPath(sessionId)

, writeTelemetry(sessionId, obj)

, and nowIso()

. The session id is sanitized (everything outside [A-Za-z0-9_-]

becomes _

), so a reading is always confined to the telemetry directory.

The pure-shell entry writes ONE file per session: the RAW Claude Code statusline payload, verbatim, at <dir>/telemetry-raw-<session>.json

. It is overwritten each render (atomically, via a temp file + rename), so a single session never grows the directory. readTelemetry(sessionId)

parses that raw file on demand. The raw payload looks like (fields vary by plan/model/version - the lib reads them straight, never assuming a shape):

{
  "session_id": "abc123",
  "context_window": { "used_percentage": 47.2, "context_window_size": 200000 },
  "rate_limits": {
    "five_hour": { "used_percentage": 12, "resets_at": 1783359600 },
    "seven_day": { "used_percentage": 30, "resets_at": 1783368000 }
  },
  "model": { "id": "claude-opus-4-1" }
}

rate_limits

is present only for Pro/Max OAuth; it is absent (so fiveHourPct

/ sevenDayPct

are omitted, never reported as 0) for API-key / Bedrock / Vertex. context_window.used_percentage

is null

before the first API response and right after a compaction (so contextPct

is null

and fresh

is false

).

Each rate_limits

window also carries resets_at

(a Unix epoch, seconds). The shell segment renders it as a compact ~time-left

countdown next to the percent: at most two units (~6d23h

, ~2h54m

, ~44m

, ~<1m

). It is a pure duration (reset minus date +%s

), so there is no clock or timezone handling. The countdown is omitted whenever resets_at

is missing (older Claude Code, or non-OAuth plans), implausible, or already in the past (a reading whose reset boundary elapsed predates the last reset, so it is treated as stale: the countdown is dropped and the bar shows this session's last-known usage % without one) - never fabricated. (readTelemetry

surfaces these reset epochs too, as fiveHourResetsAt

/ sevenDayResetsAt

; see the Consumer API section.)

telemetry-raw-<session>.json

is per session, so live sessions bound the file count, but a long-lived machine sees a new session id (UUID) per claude

run. So readTelemetry

prunes off the hot path (throttled, at most once per CCT_PRUNE_EVERY_SEC

, default hourly): it deletes raw files older than CCT_PRUNE_AGE_SEC

(default 1 day) and then caps the total at CCT_PRUNE_MAX_FILES

(default 200, deleting the oldest beyond the cap). It only ever touches this lib's own telemetry-raw-*.json

files. Pruning is best-effort and never throws.

CCT_WRAP

  • your original statusline command, as a SINGLE foreground command (a program plus args, e.g.'/path/to/your-statusline' status

). The entryexec

s it with the same stdin so it becomes the statusLine process and forwards its output. It must NOT be a pipeline (a | b

) or backgrounded (trailing&

): a pipeline mis-routes stdin, and&

leaves a process running past the render (exactly as it would running directly). If you need a pipeline, put it in a small script and pointCCT_WRAP

at that script. WhenCCT_WRAP

is unset, the entry prints its own minimal bar (ctx 47% | 5h 12% ~3h | 7d 30% ~5d | opus-4.8

).CCT_SEGMENTS

  • which bar segments to show and in what order (defaultctx,5h,7d,model

); see Customizing the bar.CCT_DIR

  • telemetry directory (default~/.claude/cc-context-telemetry/

). The shell entry mirrors this default; set it on the statusLine env AND for your hooks so both sides agree.CCT_TTL_SEC

  • freshness window forreadTelemetry

in seconds (default 120).CCT_PRUNE_AGE_SEC

  • delete raw files older than this on the reader side (default 86400, 1 day).CCT_PRUNE_MAX_FILES

  • hard cap on raw files; the oldest beyond it are deleted (default 200).CCT_PRUNE_EVERY_SEC

  • throttle: prune at most once per this interval across reader invocations (default 3600, hourly).

The raw payload file (telemetry-raw-<session>.json

) IS the verbatim statusline payload, so it doubles as the debug dump: inspect it directly to see the real field paths Claude Code sent. It may contain real session content and is overwritten each render; the reader auto-prunes it (see Raw-file hygiene).

Claude Code runs your statusLine on every render in every session and kills that process each time to keep it bounded. cct stays safe by exec

-ing your CCT_WRAP

command: the entry process BECOMES your statusline, so what Claude Code spawns and kills IS your statusline, torn down each render exactly as if you ran it directly - no extra process, no pile-up. There is also NO Node on the per-render path (only shell; all JSON parsing happens later, on demand, in index.js

), so nothing can outlive a render and orphan. An earlier design that spawned node

every render orphaned under Claude Code's render-kills and piled up; the current exec-through wrapper does not. test/loadrepro

proves it.

Full rationale (exec vs spawn, the orphan incident, the stdin/EOF contract, and the forking-statusline corollary): docs/process-safety.md.

Wire it (Quick start), open any session, and send ONE message - the statusline renders every turn, so you do not need to fill the context. (Settings changes only take effect in a FRESH session, so start a new one after saving.) Then print the parsed reading:

node examples/print-telemetry.js <session-id>

Check for sane values (a numeric or null contextPct

, a windowSize

, a model

). Your <session-id>

is the session_id

field in any hook payload, and the <session>

in the raw file written this session under ~/.claude/cc-context-telemetry/

. If values are null or missing, open that raw file - it IS the verbatim statusline payload - to see the real field paths Claude Code sent.

See the examples/ directory for complete, runnable files: a copy-pasteable PreToolUse warning hook (

pretooluse-warn.js

) and a telemetry printer for the live check (print-telemetry.js

). The examples are not published to npm.MIT

── more in #developer-tools 4 stories · sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/show-hn-cc-context-t…] indexed:0 read:12min 2026-07-17 ·