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.
Give Claude Code this gist link and tell it to install.
- π₯ 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.
π₯ β 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.
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."
- 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/claudelineSubstitute darwin_amd64, linux_amd64, linux_arm64, or a Windows zip as appropriate.
-
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.
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.
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.
#!/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()
Screenshot example
