Context Growth Watch

Model, project, a context bar with token count, what each turn costs in cache reads, how much the context has grown, and five-hour and seven-day usage. Adds a line suggesting /compact or /clear once context gets large. Python, no network calls. Keeps a small JSON file per session in ~/.claude/.statusline-state/ and deletes ones older than a week.

0 copies

In Claude Desktop? Add this status line with statusline-anywhere.

Preview

Clean repo
Opus 4.8 · app · ▕███░░░░░░░░░▏ 44k · 4.4k/turn · 5h 26% · 7d 7%
New session
Opus 4.8 · app · ctx —
Dirty branch
Sonnet 4.6 · app · ▕██████░░░░░░▏ 96k · 9.6k/turn · 5h 40% · 7d 18%
Near-full
Opus 4.8 · app · ▕███████████░▏ 182k · 18k/turn · 5h 88%↻17m · 7d 61%↻19h59m ⚠ context 91% full — /compact to keep this thread, /clear to start fresh
1M context
Fable 5 · app · ▕████████░░░░▏ 640k · 64k/turn · 5h 33% · 7d 80%↻119h59m ⚠ 640k context — every turn now costs 64k before you type. /clear if you have switched task, /compact to keep going
Post-compact
Haiku 4.5 · app · ctx — · 5h 52%↻4h01m
Worktree
Opus 4.8 · feature · ▕████░░░░░░░░▏ 74k · 7.4k/turn · 5h 44% · 7d 20%
Non-git
Opus 4.8 · scratch · ▕███░░░░░░░░░▏ 44k · 4.4k/turn

What it does

Model, project, a context bar with token count, each turn's cache-read cost, and 5-hour and 7-day usage. A second line suggests /compact or /clear once context gets large.

Written in
Python
Network access
None
Works in
Terminal

How it works

  1. Reads model, folder, total_input_tokens, context percent and rate_limits from stdin.
  2. Shows a tenth of the input tokens as each turn's cache-read cost, like 4.4k/turn.
  3. Tracks the session's smallest context and adds growth like 3.0× at 1.5 times that.
  4. Limits at 50%+ add a reset like ↻17m. At 80% full or 120k tokens, a warning line shows.

Good to know

  • Writes ~/.claude/.statusline-state/<session_id>.json. About 1 run in 37 deletes files there over a week old.
  • CC_CTX_WARN, CC_CTX_HIGH and CC_CTX_FULL_PCT change the thresholds. CC_SHOW_COST adds the cost.
  • Turn counts, a turns-left estimate and cache-miss alerts read prompt_cache fields, which no preview has.

Source

#!/usr/bin/env python3
"""
ClaudeStatsBar - a Claude Code status line that shows what a turn costs.

Tells you when a session has grown expensive enough to be worth clearing.

Why: every turn re-reads the whole accumulated context as cache reads. That is
the dominant cost in a long session (measured: 57.8% of weighted spend), and it
grows with the square of session length, because turn N re-reads everything from
turns 1..N-1. Doing the same work in several short sessions instead of one long
one is materially cheaper. This makes that visible while you work.

Install: ./install.sh  (see README.md)
Cross-platform: pure stdlib, no shell, no transcript parsing.
"""

import json
import os
import sys
import time

# --- thresholds (tokens of context). Override via env. ---
WARN = int(os.environ.get("CC_CTX_WARN", 60_000))
HIGH = int(os.environ.get("CC_CTX_HIGH", 120_000))
# fraction of the context window at which "running out of room" beats "too costly"
FULL_PCT = float(os.environ.get("CC_CTX_FULL_PCT", 80))
NO_COLOR = bool(os.environ.get("NO_COLOR"))

STATE_DIR = os.path.join(os.path.expanduser("~"), ".claude", ".statusline-state")

R = "\033[0m"
DIM = "\033[2m"
BOLD = "\033[1m"
GREEN = "\033[38;5;78m"
AMBER = "\033[38;5;214m"
RED = "\033[38;5;203m"
BLUE = "\033[38;5;110m"
GREY = "\033[38;5;245m"


def c(s, colour):
    return s if NO_COLOR else f"{colour}{s}{R}"


def human(n):
    if n is None:
        return "?"
    if n >= 1_000_000:
        return f"{n / 1_000_000:.2f}M"
    if n >= 1_000:
        return f"{n / 1000:.0f}k" if n >= 10_000 else f"{n / 1000:.1f}k"
    return str(n)


def dig(d, *path, default=None):
    """Safe nested lookup; every field here can be absent early in a session."""
    for k in path:
        if not isinstance(d, dict):
            return default
        d = d.get(k)
        if d is None:
            return default
    return d


def load_state(session_id):
    if not session_id:
        return {}
    try:
        p = os.path.join(STATE_DIR, f"{session_id}.json")
        if os.path.exists(p):
            with open(p) as f:
                return json.load(f)
    except Exception:
        pass
    return {}


def save_state(session_id, state):
    if not session_id:
        return
    try:
        os.makedirs(STATE_DIR, exist_ok=True)
        with open(os.path.join(STATE_DIR, f"{session_id}.json"), "w") as f:
            json.dump(state, f)
        # opportunistic cleanup of state files older than a week
        if int(time.time()) % 37 == 0:
            cutoff = time.time() - 7 * 86400
            for fn in os.listdir(STATE_DIR):
                fp = os.path.join(STATE_DIR, fn)
                if os.path.getmtime(fp) < cutoff:
                    os.remove(fp)
    except Exception:
        pass


def baseline(state, ctx):
    """
    Smallest context this session has been observed at, so we can show how much
    more each turn costs now than at the start. Self-calibrating: no hardcoded
    assumption about the size of your CLAUDE.md or tool schemas.

    Returns None when the status line joined an already-large session: we never
    saw the real start, so any ratio would be a reassuring lie.
    """
    lo = state.get("min_ctx")
    if lo is None or (ctx and ctx < lo):
        lo = ctx
        state["min_ctx"] = lo
    return None if (lo is None or lo >= WARN) else lo


def rate_burn(state, pct, turns):
    """
    Turns of headroom left in the 5-hour window at this session's observed burn
    rate. This, not a notional dollar figure, is the real budget on a
    subscription. Re-baselines when the rolling window resets (pct drops).
    """
    if pct is None or turns is None:
        return None
    first_pct, first_turns = state.get("rl5_pct"), state.get("rl5_turns")
    if first_pct is None or pct < first_pct or first_turns is None:
        state["rl5_pct"], state["rl5_turns"] = pct, turns
        return None
    dt, dp = turns - first_turns, pct - first_pct
    if dt < 5 or dp <= 0:  # too few samples to extrapolate honestly
        return None
    return int((100 - pct) / (dp / dt))


def bar(pct, width=12):
    pct = max(0.0, min(100.0, pct or 0.0))
    filled = int(round(width * pct / 100.0))
    return "▕" + "█" * filled + "░" * (width - filled) + "▏"


def resets_in(epoch):
    if not epoch:
        return None
    secs = int(epoch - time.time())
    if secs <= 0:
        return "now"
    if secs > 8 * 86400:  # no real limit window is longer; treat as bad data
        return None
    h, m = secs // 3600, (secs % 3600) // 60
    return f"{h}h{m:02d}m" if h else f"{m}m"


def main():
    try:
        d = json.load(sys.stdin)
    except Exception:
        print("")
        return

    ctx = dig(d, "context_window", "total_input_tokens")
    window = dig(d, "context_window", "context_window_size", default=200_000)
    used_pct = dig(d, "context_window", "used_percentage", default=0)
    model = dig(d, "model", "display_name", default="claude")
    proj = os.path.basename(
        dig(d, "workspace", "current_dir", default=dig(d, "cwd", default="")) or ""
    )
    cost = dig(d, "cost", "total_cost_usd")
    sid = dig(d, "session_id")
    turns = dig(d, "prompt_cache", "requests")

    state = load_state(sid)

    left = [c(model, BLUE)]
    if proj:
        left.append(c(proj, DIM))

    warning = None

    if ctx:
        tone = GREEN if ctx < WARN else (AMBER if ctx < HIGH else RED)
        # weighted cost of one turn's cache re-read (cache reads bill at 0.1x)
        per_turn = ctx * 0.1
        seg = f"{bar(used_pct)} {human(ctx)}"
        left.append(c(seg, tone))
        left.append(c(f"{human(int(per_turn))}/turn", tone))

        lo = baseline(state, ctx)
        if lo and ctx >= lo * 1.5:
            left.append(c(f"{ctx / lo:.1f}×", tone))

        if used_pct and used_pct >= FULL_PCT:
            warning = (
                f"context {used_pct:.0f}% full — /compact to keep this thread, "
                f"/clear to start fresh"
            )
        elif ctx >= HIGH:
            extra = f" ({ctx / lo:.1f}× its cheapest)" if lo and lo < ctx else ""
            warning = (
                f"{human(ctx)} context{extra} — every turn now costs "
                f"{human(int(per_turn))} before you type. /clear if you have "
                f"switched task, /compact to keep going"
            )
    else:
        left.append(c("ctx —", GREY))

    if turns:
        left.append(c(f"{turns}t", GREY))

    # rate limits: on a subscription this IS the budget, not the dollar estimate
    for key, label in (("five_hour", "5h"), ("seven_day", "7d")):
        pct = dig(d, "rate_limits", key, "used_percentage")
        if pct is None:
            continue
        tone = GREEN if pct < 50 else (AMBER if pct < 80 else RED)
        txt = f"{label} {pct:.0f}%"
        rst = resets_in(dig(d, "rate_limits", key, "resets_at"))
        if rst and pct >= 50:
            txt += f"↻{rst}"
        if key == "five_hour":
            burn = rate_burn(state, pct, turns)
            if burn is not None and burn < 200:
                txt += f" ~{burn}t left"
                if burn < 15:
                    tone = RED
        left.append(c(txt, tone))

    # wasted cache rebuilds: real money, invisible otherwise
    misses = dig(d, "prompt_cache", "misses")
    if misses:
        wasted = dig(d, "prompt_cache", "miss_recache_tokens")
        cause = dig(d, "prompt_cache", "last_miss_cause", "causes", default=[])
        bit = f"⚠ {misses} cache miss"
        if wasted:
            bit += f" ({human(wasted)} rebuilt)"
        if cause:
            bit += f" {cause[0]}"
        left.append(c(bit, AMBER))

    # Off by default: on a Claude subscription this is a client-side estimate at
    # API list price, not money anyone pays. The rate-limit percentages above are
    # the real budget. Set CC_SHOW_COST=1 when billing through an API key.
    if cost and os.environ.get("CC_SHOW_COST"):
        left.append(c(f"${cost:.2f}", GREY))

    save_state(sid, state)
    print(c(" · ", GREY).join(left))
    if warning:
        print(c(f"⚠  {warning}", RED if ctx >= HIGH else AMBER))


if __name__ == "__main__":
    try:
        main()
    except Exception as e:  # a broken status line must never break the session
        print(f"statusline error: {type(e).__name__}", file=sys.stderr)
        print("")


# ---------------------------------------------------------------------------
# Install:  ./install.sh          (see README.md for manual setup and Windows)
# Tune:     CC_CTX_WARN, CC_CTX_HIGH, CC_CTX_FULL_PCT, CC_SHOW_COST, NO_COLOR
# ---------------------------------------------------------------------------

More status lines

Noctu

3 copies

◆ Opus 4.8 ▏ ctx ▓▓░░░░░░░░ 22% ▏ high ▏ 10m main ▏ week ▓░░░░░░░░░ 7% ▏ 2d0h ▏ $0.41 ▏ +128/-34
◆ Opus 4.8 │ app/main ▰▰▰▰▱ 78% │ ↑44k ↓1.4k │ 5h: -- │ 7d: -- │ 10m12s
[Opus 4.8] ctx 22% · 5h ▓▓░░░░░░ 26% on track · resets in 2h06m · 7d ▓░░░░░░░ 7% on track · resets in 2d0h
Opus 4.8 · ctx ● 22% · 5h ● 26% · 7d ● 7%
✧ ███████████▋ 73% 🗝 Ultima Weapon ✦ app ∙ main ♥ ████████████████████████ 100% 「SAVE POINT」 ✶ Limit Form ◉ 41

Dreambase Panel

223 copies

╭─ CONTEXT ────────────────────────────────────────────╮ │ ████▊░░░░░░░░░░░░░░░░░ 22% of 200K · GOOD │ ├─ STATS ──────────────────────────────────────────────┤ │ ↓ 44.0K ↑ 1.4K │ $0.41 │ ⏱ 10m 12s │ +128/-34 │ ├─ REPO ───────────────────────────────────────────────┤ │ ◆ Opus 4.8 · main · 16:21 │ ╰──────────────────────────────────────────────────────╯

Related mods

Terminal

❯ Run the tests again ● The repo is clean and the tests pass. ✻ Cooked for 0s · done 10:12 PM ☀ 44k │ 5h 26% │ 7d 7% │ cache 5% [-] ❯ 

Claude Desktop

Token Weather Usage in Claude Desktop

One line above the prompt: context tokens with their weather, a bar per recent prompt, your 5-hour and 7-day limits against the time elapsed, the prompt cache with its time left, the session cost with the last prompt's share in dollars and in points of the 5-hour limit, and the subagents running. Labels in English, French or Russian. After Token Weather (Anthropic, Apache-2.0) and the ideas of usage-meter (HolyGrail) and prompt-cache-control (Daniel San).

Terminal

❯ /deck demo ⎿ deck: Sample plan running above the prompt, with sample routing until real ones arrive. [-] 0: deck ▾ · 5 rows 22:11:41 ⇄ Model route opus = opus jev: haiku · not applied effort ▁▃·· medi = kept ✕ ✓ Orders module Done · 12/12 · 2/2 agents 100% ✕ ● Context 44k / 200k 22% ✕ ◔ Session · 5h 5h · 26% used ↻ 2:04:24 ✕ ◔ Weekly · 7d 7d · 7% used ↻ 2d 0h 57m ✕ ❯  ⏵⏵ auto mode on (shift+tab to cycle) · ← for agents [ Deck 1 ]

Claude Desktop

Deck in Claude Desktop

A cockpit for Claude Code: plan bars and subagent strips, context, usage limits with reset countdowns and burn rates, spend, the model route and a pet. Six bar styles, three collapsed looks. CLI and Desktop.

Terminal

❯ /flightdeck open ⎿ flightdeck: Flightdeck opened. Focus it with ctrl+x tab; 1-6 expand cards, f/s/o open the gate rows. ╭────────────────────────────────────────────────────────────────────────────────────────────────✕─╮ │ Opus 4.8 ○ idle · ctx ▰▱▱▱▱▱ 22% · $0.25 │ │ last turn 0s · 0 agents · 0 edits · 0 errors │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ❯  ⚠ flightdeck: ctx 22%

Claude Desktop

Flightdeck in Claude Desktop

Flightdeck: a live agent dashboard for Claude Code. Main model vitals, an on-call architect, every permission check, subagent cards and swimlanes, a turn receipt and a session log, all from real session events

Terminal

[-] zZ ▀▄ ▄▀ ▄▄▀▀▀▀▀▀▀▀▀▀ ▀▀▀▄▄▄ ▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀ ctx 22% [ Pet ♥ ] [ ⚙ ] [ ↻ Adventurer ] [ Hide ] ❯ 

Claude Desktop

Pixel Cat in Claude Desktop

A little pixel cat that wanders above the Claude Code prompt, watches your usage limits and announces when work finishes; or switch packs for an adventurer who mines, chops trees and fights zombies