My custom Claude Code Statusline (via claudeline)
A Python wrapper around claudeline that adds a true-1M context %, a custom alert threshold for the context window, the reasoning effort level, 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
- Reasoning effort level after the model, in place of claudeline's subscription-plan label β read live from the payload's
effort.level, so it tracks/effortchanges mid-session (low/medium/high/xhigh/max) - 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 β Opus 4.7 β XHigh β ββββββββββ 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, effort level, 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 7-day quota bar and drops it (the model name, which also contains "sonnet", is kept). Detects π₯ anywhere and routes it to the left. Drops claudeline's subscription-plan label and reads the reasoning effort level from the payload's
effort.levelinstead, rendered right after the model. - 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, appends the folder, branch, dirty count, and todo summary, then wraps the whole line to the detected terminal width.
- 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.EFFORT_LABELS- display labels for each effort level (low/medium/high/xhigh/max); unknown values fall through as-is.
#!/usr/bin/env python3
"""Custom statusline wrapper around claudeline.
Layout (left to right):
session-duration β model β effort β 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: show used % (bar grows with usage; 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. Preserve emoji warnings claudeline emits inside each segment.
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.
10. Replace claudeline's subscription-plan segment with the live reasoning
effort level (payload effort.level), placed after the model.
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
# Display labels for the reasoning effort level (payload effort.level).
# Falls back to the raw value for anything not listed here.
EFFORT_LABELS = {
"low": "Low",
"medium": "Medium",
"high": "High",
"xhigh": "XHigh",
"max": "Max",
}
EXTENDED_TOKEN_THRESHOLD = 300_000
TRUE_CONTEXT_WINDOW = 1_000_000
TRANSCRIPT_TAIL_BYTES = 512 * 1024
HYPERLINK_RE = re.compile(r"\x1b\]8;[^\x07]*\x07")
def visible_len(s: str) -> int:
s = ANSI_RE.sub("", s)
s = HYPERLINK_RE.sub("", s)
total = 0
for ch in s:
cp = ord(ch)
if cp >= 0x1F000 or 0x2300 <= cp <= 0x23FF or 0x2B00 <= cp <= 0x2BFF:
total += 2
else:
total += 1
return total
def _size_from_tty(tty: str) -> int | None:
if not tty or "?" in tty:
return None
try:
fd = os.open(f"/dev/{tty}", os.O_RDONLY | os.O_NOCTTY | os.O_NONBLOCK)
except Exception:
return None
try:
cols = os.get_terminal_size(fd).columns
return cols if cols > 0 else None
except Exception:
return None
finally:
try:
os.close(fd)
except Exception:
pass
def terminal_width(default: int = 10000) -> int:
for fd in (1, 2, 0):
try:
cols = os.get_terminal_size(fd).columns
if cols > 0:
return cols
except Exception:
continue
try:
fd = os.open("/dev/tty", os.O_RDONLY | os.O_NOCTTY)
try:
cols = os.get_terminal_size(fd).columns
if cols > 0:
return cols
finally:
os.close(fd)
except Exception:
pass
candidates: list[int] = []
pid_env = os.environ.get("CMUX_CLAUDE_PID")
if pid_env and pid_env.isdigit():
candidates.append(int(pid_env))
candidates.append(os.getpid())
seen: set[int] = set()
for start in candidates:
cur = start
for _ in range(12):
if cur <= 1 or cur in seen:
break
seen.add(cur)
try:
r = subprocess.run(
["ps", "-p", str(cur), "-o", "tty=,ppid="],
capture_output=True,
text=True,
timeout=1,
)
except Exception:
break
parts = r.stdout.split()
if not parts:
break
cols = _size_from_tty(parts[0])
if cols:
return cols
if len(parts) >= 2 and parts[1].isdigit():
cur = int(parts[1])
else:
break
cols = os.environ.get("COLUMNS")
if cols and cols.isdigit() and int(cols) > 0:
return int(cols)
return default
def wrap_line(parts: list[str], sep: str, width: int) -> str:
if not parts:
return ""
sep_len = visible_len(sep)
lines: list[list[str]] = []
current: list[str] = []
current_len = 0
for p in parts:
plen = visible_len(p)
if not current:
current.append(p)
current_len = plen
continue
if current_len + sep_len + plen <= width:
current.append(p)
current_len += sep_len + plen
else:
lines.append(current)
current = [p]
current_len = plen
if current:
lines.append(current)
return "\n".join(sep.join(line) for line in lines)
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()
# Drop only the sonnet 7-day quota *bar* (transform #1). A bare
# "sonnet" substring match also ate the "Sonnet 4.6" model text
# segment, leaving the line model-less when Sonnet was selected.
if "sonnet" in plain.lower() and ("β" in plain or "β" in plain):
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)
# text_segments[0] is claudeline's subscription-plan label (e.g. "Max").
# We no longer display it; the effort level takes its place.
model = text_segments[1] if len(text_segments) >= 2 else ""
extra_texts = text_segments[2:]
effort = ""
eff = payload.get("effort")
if isinstance(eff, dict):
level = eff.get("level")
if isinstance(level, str) and level:
effort = EFFORT_LABELS.get(level.lower(), level)
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 model:
parts.append(f"{CYAN}{model}{RESET}")
if effort:
parts.append(f"{CYAN}{effort}{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}"
if project:
parts.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}"
parts.append(f"{DIM}{label}{RESET}")
todo_summary = render_todos(todo_counts(transcript_path))
if todo_summary:
parts.append(f"{DIM}{todo_summary}{RESET}")
width = terminal_width()
sys.stdout.write(wrap_line(parts, sep, width))
if __name__ == "__main__":
main()
Screenshot example
