Skip to content

Instantly share code, notes, and snippets.

@Reebz
Last active May 25, 2026 06:02
Show Gist options
  • Select an option

  • Save Reebz/741c5647c860fe0b5214f39d9d887240 to your computer and use it in GitHub Desktop.

Select an option

Save Reebz/741c5647c860fe0b5214f39d9d887240 to your computer and use it in GitHub Desktop.
My custom Claude Code Statusline (via claudeline)

Custom statusline for Claude Code (claudeline wrapper)

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.

image

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 usage block 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 ⏳M or No tasks), reading TodoWrite and TaskCreate/TaskUpdate events 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

  1. Save statusline.py (below) to ~/.claude/bin/statusline.py.

  2. Add this block to ~/.claude/settings.json at the top level:

    {
      "statusLine": {
        "type": "command",
        "command": "python3 /Users/YOUR_USERNAME/.claude/bin/statusline.py"
      }
    }

    Replace YOUR_USERNAME with your account (or use the absolute path to your home dir). Claude Code does not expand ~ in this field.

  3. (Optional) Add CLAUDE_CODE_AUTO_COMPACT_WINDOW to the env block as shown above.

  4. 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:

  1. Forwards the JSON to ~/.claude/bin/claudeline and captures its formatted line.
  2. Splits the output on β”‚ / Β· separators while preserving ANSI codes.
  3. Detects the sonnet segment and drops it. Detects πŸ”₯ anywhere and routes it to the left.
  4. Parses each remaining bar segment (β–ˆβ–‘ glyphs + N% + optional (time)).
  5. Tails the transcript jsonl (last 512KB) for the latest assistant usage block and sums input_tokens + cache_creation_input_tokens + cache_read_input_tokens into a true context-token count.
  6. Overrides the context bar's percentage with tokens / 1_000_000. Strips πŸ₯΅ from the bar's extras unless tokens >= 300_000.
  7. Re-renders bars at custom widths and inversion direction, rounds times, and adds the right-side metadata (folder, branch, dirty count, todos).
  8. Reads transcript_path again for the first timestamp (session start) and for TodoWrite / TaskCreate / TaskUpdate events (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 to 200_000 if 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 latest usage block.

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()
@Reebz
Copy link
Copy Markdown
Author

Reebz commented May 15, 2026

Screenshot example
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment