A Python wrapper around claudeline that adds a true-1M context %, a custom alert threshold for the context window, and a few layout changes. Reads the transcript jsonl directly for session duration, context tokens, and todos. Shells out to git for branch and dirty state. Inverts the quota bars from used to remaining, reformats times to rounded 12-hour, doubles the context bar width with wrapping, and adds an incident link.
TL;DR
Give Claude Code this gist link and tell it to install.
What it does
- ๐ฅ service-disruption indicator on the far left (links to https://status.claude.com), hidden when no incident
- Session duration on the far left, parsed from the transcript jsonl
- Sonnet 7-day quota bar dropped
- Context window shown as % of true 1M model capacity, computed from the latest
usageblock in the transcript (input + cache_creation + cache_read). Bar fill and number both reflect this. claudeline's color zones (green โ yellow โ orange โ red) pass through, still reflecting risk relative to the configured compaction window. - ๐ฅต (extended context) only shown above 300k tokens, overriding claudeline's hardcoded 200k threshold. claudeline still emits ๐ฅต at 200k. The wrapper strips it below 300k.
- 5-hour and 7-day quotas shown as remaining (drain as you use)
- Reset times rounded to the nearest hour, lowercase 12-hour (1am, 2pm)
- Project folder + git branch + dirty-repo dot on the right
- TODO summary on the far right (
โถN โณMorNo tasks), readingTodoWriteandTaskCreate/TaskUpdateevents from the transcript โseparators throughout
Stays on the claudeline upgrade path. Only consumes its stdout and reads the live transcript.
Example
๐ฅ โ 1h05m โ Max โ Opus 4.7 โ โโโโโโโโโโ 21% โ โโโโโ 95% (3pm) โ โโโโโ 68% (Mon 9am) โ frontend-slides โ main 3โ โ โถ1 โณ2
Here 21% = 210k tokens of true 1M. The bar fills toward compaction at ~85% (because of the CLAUDE_CODE_AUTO_COMPACT_WINDOW=850000 env var below), so 85% on this bar means compact is firing.
Companion env var: earlier auto-compact
claudeline only reads context state. It can't change when Claude Code auto-compacts. To move compaction earlier, set CLAUDE_CODE_AUTO_COMPACT_WINDOW in ~/.claude/settings.json:
{
"env": {
"CLAUDE_CODE_AUTO_COMPACT_WINDOW": "850000"
}
}Claude Code treats this as the effective context window and fires auto-compact when ~13k tokens remain. With 850000, compact fires at ~837k tokens, about 84% of true 1M for Opus 4.7 extended context. The value is capped at the model's real max, so on a 200k model this is a no-op.
The wrapper bar is independent of this (always tokens / 1M), so 85% genuinely means "compact is about to happen."
Prereqs
- Python 3.9+ (preinstalled on macOS)
- claudeline binary at
~/.claude/bin/claudeline
Install claudeline via the official Claude Code plugin:
/plugin marketplace add fredrikaverpil/claudeline
/plugin install claudeline@claudeline
/claudeline:setup
Or download a release directly (macOS arm64 example):
mkdir -p ~/.claude/bin curl -fsSL -o /tmp/claudeline.tar.gz \ https://github.com/fredrikaverpil/claudeline/releases/latest/download/claudeline_darwin_arm64.tar.gz tar -xzf /tmp/claudeline.tar.gz -C ~/.claude/bin/ chmod +x ~/.claude/bin/claudeline
Substitute darwin_amd64, linux_amd64, linux_arm64, or a Windows zip as appropriate.
Install the wrapper
-
Save
statusline.py(below) to~/.claude/bin/statusline.py. -
Add this block to
~/.claude/settings.jsonat the top level:{ "statusLine": { "type": "command", "command": "python3 /Users/YOUR_USERNAME/.claude/bin/statusline.py" } }Replace
YOUR_USERNAMEwith your account (or use the absolute path to your home dir). Claude Code does not expand~in this field. -
(Optional) Add
CLAUDE_CODE_AUTO_COMPACT_WINDOWto theenvblock as shown above. -
Restart Claude Code.
How it works
Claude Code pipes a JSON payload (session id, transcript path, model, cwd, etc.) to the configured statusLine.command on every render. The wrapper:
- Forwards the JSON to
~/.claude/bin/claudelineand captures its formatted line. - Splits the output on
โ/ยทseparators while preserving ANSI codes. - Detects the sonnet segment and drops it. Detects ๐ฅ anywhere and routes it to the left.
- Parses each remaining bar segment (
โโglyphs +N%+ optional(time)). - Tails the transcript jsonl (last 512KB) for the latest assistant
usageblock and sumsinput_tokens + cache_creation_input_tokens + cache_read_input_tokensinto a true context-token count. - Overrides the context bar's percentage with
tokens / 1_000_000. Strips ๐ฅต from the bar's extras unlesstokens >= 300_000. - Re-renders bars at custom widths and inversion direction, rounds times, and adds the right-side metadata (folder, branch, dirty count, todos).
- Reads
transcript_pathagain for the first timestamp (session start) and forTodoWrite/TaskCreate/TaskUpdateevents (todo summary).
If anything fails (claudeline missing, transcript unreadable, etc.) the wrapper exits silently so Claude Code keeps the previous statusline.
Tunables
In the constants block near the top of statusline.py:
TRUE_CONTEXT_WINDOW = 1_000_000- denominator for the bar %. Set to200_000if you're not on extended-context Opus.EXTENDED_TOKEN_THRESHOLD = 300_000- minimum token count before ๐ฅต is allowed through.CTX_WIDTH = 10- context bar width in cells.QUOTA_WIDTH = 5- 5h / 7d quota bar widths.TRANSCRIPT_TAIL_BYTES = 512 * 1024- how much of the transcript to scan for the latestusageblock.
statusline.py
#!/usr/bin/env python3 """Custom statusline wrapper around claudeline. Layout (left to right): session-duration โ plan โ model โ ctx-bar โ 5h-bar โ 7d-bar โ folder โ branch โ todo-summary Transformations vs. raw claudeline: 1. Drop the sonnet 7-day quota bar. 2. Context window: bar % overridden to tokens / TRUE_CONTEXT_WINDOW (default 1M), computed from the latest `usage` block in the transcript jsonl. Color zone preserved. 3. 5h and 7d quotas: show remaining % (bars fill toward 100%). 4. Round times to nearest hour, lowercase 12-hour (1am, 2pm). 5. Double-width context window bar (5 -> 10 cells). 6. Strip claudeline's ๐ฅต (fires at 200k) unless context tokens >= EXTENDED_TOKEN_THRESHOLD (300k). 7. Folder name and git branch on the right, with dirty-repo indicator. 8. Session duration on the far left, parsed from the transcript jsonl. 9. TODO summary on the far right (โถ in_progress, โณ pending), hidden when empty. Falls back to silent exit on any error (Claude Code keeps the previous line). """ from __future__ import annotations import json import os import re import subprocess import sys from datetime import datetime, timezone CLAUDELINE_BIN = os.path.expanduser("~/.claude/bin/claudeline") ANSI_RE = re.compile(r"\x1b\[[0-9;]*m") SEP_RE = re.compile(r"\x1b\[2m\s*[โยท]\s*\x1b\[0m") COLOR_RE = re.compile(r"\x1b\[(?:3[0-7]|9[0-7])m") BAR_RE = re.compile(r"([โโ]+)\s+(\d+)%") TIME_RE = re.compile(r"\(([^)]+)\)") CYAN = "\x1b[36m" YELLOW = "\x1b[33m" BLUE = "\x1b[94m" DIM = "\x1b[2m" RESET = "\x1b[0m" CTX_WIDTH = 10 QUOTA_WIDTH = 5 EXTENDED_TOKEN_THRESHOLD = 300_000 TRUE_CONTEXT_WINDOW = 1_000_000 TRANSCRIPT_TAIL_BYTES = 512 * 1024 def render_bar(pct: int, width: int, color: str) -> str: filled = round(pct / 100 * width) filled = max(0, min(width, filled)) empty = width - filled return f"{color}{'โ' * filled}{DIM}{'โ' * empty}{RESET}" def round_to_hour(hhmm: str) -> str: h_str, m_str = hhmm.split(":") h, m = int(h_str), int(m_str) if m >= 30: h = (h + 1) % 24 if h == 0: return "12am" if h < 12: return f"{h}am" if h == 12: return "12pm" return f"{h - 12}pm" def format_time(t: str) -> str: tokens = t.split() try: if len(tokens) == 1 and ":" in tokens[0]: return round_to_hour(tokens[0]) if len(tokens) == 2 and ":" in tokens[1]: return f"{tokens[0]} {round_to_hour(tokens[1])}" except Exception: pass return t def git_branch(cwd: str) -> str: try: r = subprocess.run( ["git", "-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"], capture_output=True, text=True, timeout=2, ) if r.returncode == 0: return r.stdout.strip() except Exception: pass return "" def git_dirty_count(cwd: str) -> int: try: r = subprocess.run( ["git", "-C", cwd, "status", "--porcelain"], capture_output=True, text=True, timeout=2, ) if r.returncode == 0: return sum(1 for line in r.stdout.splitlines() if line.strip()) except Exception: pass return 0 def parse_bar_segment(raw_seg: str) -> dict | None: plain = ANSI_RE.sub("", raw_seg).strip() bar_match = BAR_RE.search(plain) if not bar_match: return None bar = bar_match.group(1) pct = int(bar_match.group(2)) time_match = TIME_RE.search(plain) time_str = time_match.group(1) if time_match else None extras = plain extras = BAR_RE.sub("", extras, count=1) if time_match: extras = extras.replace(f"({time_match.group(1)})", "", 1) extras = re.sub(r"\bsonnet\b", "", extras) extras = re.sub(r"\s+", " ", extras).strip() color_match = COLOR_RE.search(raw_seg) color = color_match.group(0) if color_match else BLUE return { "width": len(bar), "pct": pct, "time": time_str, "extras": extras, "color": color, } def render_segment( parsed: dict, width: int, mode: str, override_pct: int | None = None, strip_extended: bool = False, ) -> str: if override_pct is not None: shown = override_pct elif mode == "used": shown = parsed["pct"] else: shown = 100 - parsed["pct"] bar = render_bar(shown, width, parsed["color"]) out = f"{bar} {shown}%" if parsed["time"]: out += f" ({format_time(parsed['time'])})" extras = parsed["extras"] if strip_extended: extras = extras.replace("๐ฅต", "") extras = re.sub(r"\s+", " ", extras).strip() if extras: out += f" {extras}" return out def latest_context_tokens(transcript_path: str) -> int: if not transcript_path or not os.path.exists(transcript_path): return 0 try: size = os.path.getsize(transcript_path) offset = max(0, size - TRANSCRIPT_TAIL_BYTES) with open(transcript_path) as f: f.seek(offset) if offset > 0: f.readline() lines = f.readlines() except Exception: return 0 last_usage = None for line in lines: try: ev = json.loads(line) except Exception: continue msg = ev.get("message") if not isinstance(msg, dict): continue u = msg.get("usage") if isinstance(u, dict): last_usage = u if not last_usage: return 0 keys = ("input_tokens", "cache_creation_input_tokens", "cache_read_input_tokens") return sum(int(last_usage.get(k, 0) or 0) for k in keys) def session_duration(transcript_path: str) -> str: if not transcript_path or not os.path.exists(transcript_path): return "" start_ts = None try: with open(transcript_path) as f: for _ in range(50): line = f.readline() if not line: break try: ev = json.loads(line) except Exception: continue ts = ev.get("timestamp") if isinstance(ts, str): try: start_ts = datetime.fromisoformat( ts.replace("Z", "+00:00") ).timestamp() break except Exception: continue except Exception: return "" if start_ts is None: return "" seconds = max(0, int(datetime.now(timezone.utc).timestamp() - start_ts)) h = seconds // 3600 m = (seconds % 3600) // 60 s = seconds % 60 if h > 0: return f"{h}h{m:02d}m" if m > 0: return f"{m}m{s:02d}s" return f"{s}s" def todo_counts(transcript_path: str) -> dict[str, int]: counts: dict[str, int] = {} if not transcript_path or not os.path.exists(transcript_path): return counts latest_todowrite = None create_count = 0 statuses: dict[str, str] = {} try: with open(transcript_path) as f: for line in f: try: ev = json.loads(line) except Exception: continue msg = ev.get("message") or {} content = msg.get("content") or [] if not isinstance(content, list): continue for c in content: if not isinstance(c, dict) or c.get("type") != "tool_use": continue name = c.get("name", "") inp = c.get("input") or {} if name == "TodoWrite": latest_todowrite = inp.get("todos") or [] elif name == "TaskCreate": create_count += 1 tid = str(create_count) statuses.setdefault(tid, "pending") elif name == "TaskUpdate": tid = inp.get("taskId") or inp.get("id") st = inp.get("status") if tid: statuses[str(tid)] = st or statuses.get(str(tid), "pending") elif name == "TaskStop": tid = inp.get("taskId") or inp.get("id") if tid: statuses[str(tid)] = "canceled" except Exception: return counts if latest_todowrite: for t in latest_todowrite: s = (t.get("status") or "pending").lower() counts[s] = counts.get(s, 0) + 1 return counts for s in statuses.values(): counts[s] = counts.get(s, 0) + 1 return counts def render_todos(counts: dict[str, int]) -> str: in_prog = counts.get("in_progress", 0) pending = counts.get("pending", 0) if in_prog == 0 and pending == 0: return "No tasks" parts = [] if in_prog: parts.append(f"{YELLOW}โถ{in_prog}{RESET}{DIM}") if pending: parts.append(f"โณ{pending}") return " ".join(parts) def main() -> None: stdin_data = sys.stdin.read() try: payload = json.loads(stdin_data) except Exception: payload = {} workspace = payload.get("workspace") or {} cwd = workspace.get("project_dir") or payload.get("cwd") or os.getcwd() project = os.path.basename(cwd.rstrip("/")) if cwd else "" transcript_path = payload.get("transcript_path") or "" try: result = subprocess.run( [CLAUDELINE_BIN], input=stdin_data, capture_output=True, text=True, timeout=10, ) raw = result.stdout.strip("\n") except Exception: return if not raw: return raw_segments = [s for s in SEP_RE.split(raw) if s.strip()] has_incident = "๐ฅ" in raw text_segments: list[str] = [] bar_parses: list[dict] = [] for seg in raw_segments: plain = ANSI_RE.sub("", seg).strip() if "sonnet" in plain.lower(): continue if "๐ฅ" in plain: continue if "โ" in plain or "โ" in plain: parsed = parse_bar_segment(seg) if parsed: bar_parses.append(parsed) else: text_segments.append(plain) plan = text_segments[0] if len(text_segments) >= 1 else "" model = text_segments[1] if len(text_segments) >= 2 else "" extra_texts = text_segments[2:] parts: list[str] = [] if has_incident: link = "\x1b]8;;https://status.claude.com\x07" link_close = "\x1b]8;;\x07" parts.append(f"{link}๐ฅ{link_close}") duration = session_duration(transcript_path) if duration: parts.append(f"{DIM}{duration}{RESET}") if plan: parts.append(f"{CYAN}{plan}{RESET}") if model: parts.append(f"{CYAN}{model}{RESET}") if len(bar_parses) >= 1: ctx_tokens = latest_context_tokens(transcript_path) strip_ext = ctx_tokens < EXTENDED_TOKEN_THRESHOLD true_pct = min(100, round(ctx_tokens / TRUE_CONTEXT_WINDOW * 100)) if ctx_tokens else None parts.append( render_segment( bar_parses[0], CTX_WIDTH, mode="used", override_pct=true_pct, strip_extended=strip_ext, ) ) if len(bar_parses) >= 2: parts.append(render_segment(bar_parses[1], QUOTA_WIDTH, mode="remaining")) if len(bar_parses) >= 3: parts.append(render_segment(bar_parses[2], QUOTA_WIDTH, mode="remaining")) for extra in extra_texts: parts.append(f"{DIM}{extra}{RESET}") sep = f"{DIM} โ {RESET}" line = sep.join(parts) right: list[str] = [] if project: right.append(f"{CYAN}{project}{RESET}") if cwd: branch = git_branch(cwd) if branch: dirty = git_dirty_count(cwd) label = branch if dirty == 0 else f"{branch} {YELLOW}{dirty}โ{RESET}{DIM}" right.append(f"{DIM}{label}{RESET}") todo_summary = render_todos(todo_counts(transcript_path)) if todo_summary: right.append(f"{DIM}{todo_summary}{RESET}") if right: line += sep + sep.join(right) sys.stdout.write(line) if __name__ == "__main__": main()