Status Line Scripts skill for Claude Code

Context, cost and branch at a glance, under 50 ms. Claude builds a status line from the documented stdin contract instead of guessing field names, treats every optional field as possibly absent, caches git calls per session so the bar stays fast, and pipe-tests full, empty and malformed payloads before registering it. Ships a stdlib Python status line with a color-coded context bar, cost and 5-hour limit you can adapt.

  • namestatusline-scripts
  • size165 lines · ≈1,950 tokens
  • files2
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Builds custom Claude Code status lines configured under statusLine in settings.json, as shell, Python, or Node scripts that read the session JSON on stdin and print model, directory, git branch and dirty state, context window percentage, session cost, and rate-limit usage, with colors, multiple lines, caching, and sub-50 ms runtime. Use when the user asks for a status bar, wants to see context or cost at a glance, runs /statusline, or a status line is blank, slow, or garbled.

Try asking

  • Give me a status line with model, git branch, context percentage and session cost
  • My status line lags behind and sometimes shows stale branch info — speed it up
  • Add a red warning to the status bar when I pass 80% of my 5-hour limit

Use it when

  • You want context usage, cost, or branch visible all the time
  • A team wants a shared status line committed to the repo
  • An existing status line is blank, slow, or garbled
  • You need different segments when Claude works inside a worktree or subdirectory

Skip it when

  • You only need clickable ticket or PR links in the footer — configure footerLinksRegexes in settings instead
  • You want to cut token spend rather than watch it — use usage-cost-tracking instead

How to check the result

Pipe a full payload, an empty object and invalid JSON through the command: each prints a line and exits 0, and a timed warm run stays under 50 ms.

The common mistake it prevents

Keying the git cache on the process ID, which changes on every run, so git status executes after every message and the bar lags or gets cancelled.

~/.claude/skills/statusline-scripts/SKILL.md

--- name: statusline-scripts description: Builds custom Claude Code status lines configured under statusLine in settings.json, as shell, Python, or Node scripts that read the… license: MIT ---

A status line is done when it shows the three to five facts the user actually glances at, handles every optional field being absent, finishes in under 50 ms on a warm cache, never prints a traceback, and was tested by piping sample JSON before being registered.

Quick path vs. hand-written

SituationDo this
User wants something reasonable fast/statusline with a description (“model, branch, context %”) or no argument to derive one from the shell prompt
Specific segments, team-shared script, or performance mattersWrite a script (below); start from scripts/statusline.py
Only want clickable IDs (tickets, PRs) in the footerfooterLinksRegexes setting instead of a script
Per-subagent row formatting in the agent panelsubagentStatusLine (separate contract: emits one JSON line per row)

Contract

Configure in ~/.claude/settings.json (personal) or .claude/settings.json (team):

{
  "statusLine": {
    "type": "command",
    "command": "python3 ~/.claude/statusline.py",
    "padding": 0,
    "refreshInterval": 10
  }
}
  • command runs in a shell; inline commands work. padding adds horizontal indent. refreshInterval (seconds, min 1) re-runs on a timer as well as on events; set it only for clocks or externally changing data.
  • Runs at session start, then on new assistant messages, after /compact, on permission or vim mode changes, and when the command changes. Updates are debounced at 300 ms and an in-flight run is cancelled when a new one triggers, so a slow script shows stale data.
  • Every line printed to stdout is one row. ANSI colors and OSC 8 hyperlinks are rendered.
  • tput cols cannot see the terminal; read $COLUMNS / $LINES, which are set for you.
  • On Windows the command runs through Git Bash if present, else PowerShell. Use forward slashes in paths.

Input fields worth using

FieldNotes
model.display_name, model.idAlways present
workspace.current_dir, workspace.project_dirDiffer when Claude has cd’d; show both when they differ
workspace.added_dirsDirectories from --add-dir
workspace.git_worktree, worktree.*Present only inside linked worktrees / worktree sessions
context_window.used_percentageInput tokens only; null early in a session and after /compact
context_window.context_window_size200000 or 1000000
cost.total_cost_usdClient-side list-price estimate; resets on /clear
cost.total_duration_ms, cost.total_lines_added/removedSession stats
rate_limits.five_hour.used_percentage, .seven_day.*, .resets_atSubscribers only, after the first response; each window may be absent
prompt_cache.warm, prompt_cache.hit_ratioRecent versions; absent until first response
output_style.name, agent.name, vim.modeOptional
session_idStable per session; use it to key caches

Treat everything except model and workspace as optional. In jq use // empty or // 0; in Python use a safe getter.

Workflow

- [ ] 1. Ask which 3-5 facts matter (default: model, dir, branch*, ctx %, cost)
- [ ] 2. Copy scripts/statusline.py (or write bash/node) to ~/.claude/ and adapt
- [ ] 3. Pipe sample JSON through it: full payload, empty {}, malformed input
- [ ] 4. Time it: under 50 ms warm, under 200 ms cold
- [ ] 5. Register in settings.json; confirm it renders in a live session
- [ ] 6. Check behavior after /compact (ctx null) and in a non-git directory

The bundled script prints model, directory (with project prefix when Claude has moved into a subdirectory), branch with * when dirty, a colored context bar (green < 50%, yellow < 80%, red above), cost, and the 5-hour limit when present. It caches git output per session_id for 5 seconds:

cp scripts/statusline.py ~/.claude/statusline.py
echo '{"session_id":"t1","model":{"display_name":"Opus"},"workspace":{"current_dir":"'"$PWD"'"},"context_window":{"used_percentage":63},"cost":{"total_cost_usd":0.43}}' \
  | python3 ~/.claude/statusline.py
echo '{}' | python3 ~/.claude/statusline.py        # must still print a line, exit 0

Env toggles: STATUSLINE_TWO_LINES=1, STATUSLINE_NO_COLOR=1, STATUSLINE_GIT_TTL=5.

Minimal bash version (needs jq)

#!/usr/bin/env bash
input=$(cat)
model=$(jq -r '.model.display_name // "?"' <<<"$input")
dir=$(jq -r '.workspace.current_dir // .cwd' <<<"$input")
pct=$(jq -r '.context_window.used_percentage // empty' <<<"$input")
cost=$(jq -r '.cost.total_cost_usd // empty' <<<"$input")
branch=$(git -C "$dir" branch --show-current 2>/dev/null)
line="[$model] ${dir##*/}"
[ -n "$branch" ] && line+=" | $branch"
[ -n "$pct" ] && line+=" | ctx ${pct%.*}%"
[ -n "$cost" ] && line+=" | \$$(printf '%.2f' "$cost")"
printf '%s\n' "$line"

Each jq call forks a process (~5 ms each). Past four fields, parse once:

IFS=$'\x1f' read -r model dir pct cost < <(jq -r '[.model.display_name, (.workspace.current_dir // .cwd),
  (.context_window.used_percentage // ""), (.cost.total_cost_usd // "")] | map(tostring) | join("\u001f")' <<<"$input")

Use a non-whitespace separator: with a tab IFS, read collapses empty fields and shifts every later value.

Performance rules

  • Budget: the script runs after every assistant message. Anything over ~100 ms makes the bar visibly lag and get cancelled.
  • Cache expensive work (git status, network calls, kubectl) in a temp file keyed by session_id, never by PID ($$ changes every run and defeats the cache).
  • Use git status --porcelain --untracked-files=no in large repos; untracked scans are the slow part. Add timeout guards (1 s) around subprocesses.
  • No network calls on the hot path. If you must show remote data (CI status, on-call), refresh it from a background job into a file and only read the file here.
  • Python startup is ~20–30 ms; Node ~40 ms; bash with one jq ~10 ms. All acceptable.

Gotchas

  • used_percentage excludes output tokens; computing your own from total_input_tokens plus output will disagree with /context. Use the provided field.
  • The cost figure is an estimate at list price, not a bill; label it as such for teams.
  • rate_limits appears only for Pro/Max subscribers (or behind a spend-limited gateway); API-key users never get it. Do not show an empty “5h —” placeholder.
  • echo -e handles escapes inconsistently across shells; use printf '%b' for colors and OSC 8 links.
  • Emoji and wide glyphs can misalign in some terminals; keep them optional.
  • A committed team status line runs on every teammate’s machine. Use only tools you know are installed, or degrade gracefully when jq/git are missing.
  • Status line commands are subject to the workspace trust dialog and disableAllHooks; if nothing renders in a fresh clone, accept trust first.

Verification

  1. Pipe tests: full payload, {}, invalid JSON, a non-git cwd — each prints one or more lines and exits 0.
  2. time (echo '<payload>' | <command>) under 50 ms on the second run.
  3. Live session: send a message, run /compact, cd into a subdirectory via Claude, and confirm each segment updates or hides cleanly.
  4. If it is blank: run the exact command string in a terminal with sample JSON, check the script is executable and the path expands (~ works; quote paths with spaces), and check claude --debug output for the status line command error.

Output

**Status line:** `python3 ~/.claude/statusline.py` (user settings)
**Shows:** [Opus] repo/…/subdir | feat/login* | ctx ██████░░░░ 63% | $0.43 | 5h 82%
**Tested:** full / empty / malformed / non-git payloads; 31 ms warm, 64 ms cold
**Caching:** git info per session_id, 5 s TTL

Bundled files

Loaded only when a step in the skill needs them. Review them here before installing.

scripts/statusline.py 114 lines · 3.8 KB
#!/usr/bin/env python3
"""Claude Code status line: model | dir | git branch+dirty | context bar | cost | 5h limit.

Reads the status line JSON from stdin and prints one or two lines.
Stdlib only. Never raises: on any error it prints a minimal fallback line.

Usage in settings.json:
  {"statusLine": {"type": "command", "command": "python3 ~/.claude/statusline.py"}}

Options (env vars):
  STATUSLINE_TWO_LINES=1   put context/cost on a second line
  STATUSLINE_NO_COLOR=1    disable ANSI colors
  STATUSLINE_GIT_TTL=5     seconds to cache git info per session (default 5)
"""
import json
import os
import subprocess
import sys
import tempfile
import time

NO_COLOR = os.environ.get("STATUSLINE_NO_COLOR") == "1"
GIT_TTL = float(os.environ.get("STATUSLINE_GIT_TTL", "5"))


def c(code: str, text: str) -> str:
    return text if NO_COLOR else f"\033[{code}m{text}\033[0m"


def get(d, path, default=None):
    for key in path.split("."):
        if not isinstance(d, dict) or d.get(key) is None:
            return default
        d = d[key]
    return d


def git_info(cwd: str, session_id: str) -> str:
    """Return 'branch*' (dirty) or 'branch', cached per session to stay fast."""
    safe_id = "".join(ch for ch in session_id if ch.isalnum() or ch in "-_") or "nosession"
    cache = os.path.join(tempfile.gettempdir(), f"statusline-git-{safe_id}")
    try:
        if time.time() - os.path.getmtime(cache) < GIT_TTL:
            with open(cache, encoding="utf-8") as fh:
                return fh.read()
    except OSError:
        pass
    result = ""
    try:
        run = lambda *a: subprocess.run(  # noqa: E731
            ["git", "-C", cwd, *a], capture_output=True, text=True, timeout=1
        )
        branch = run("branch", "--show-current")
        if branch.returncode == 0:
            name = branch.stdout.strip() or "(detached)"
            dirty = run("status", "--porcelain", "--untracked-files=no").stdout.strip()
            result = name + ("*" if dirty else "")
    except (OSError, subprocess.SubprocessError):
        result = ""
    try:
        with open(cache, "w", encoding="utf-8") as fh:
            fh.write(result)
    except OSError:
        pass
    return result


def bar(pct: float, width: int = 10) -> str:
    filled = max(0, min(width, round(pct / 100 * width)))
    color = "32" if pct < 50 else "33" if pct < 80 else "31"
    return c(color, "█" * filled + "░" * (width - filled)) + f" {pct:.0f}%"


def main() -> None:
    data = json.loads(sys.stdin.read() or "{}")
    model = get(data, "model.display_name", "?")
    cwd = get(data, "workspace.current_dir") or data.get("cwd") or os.getcwd()
    project = get(data, "workspace.project_dir", cwd)
    session = data.get("session_id", "")

    where = os.path.basename(cwd.rstrip("/")) or cwd
    if project and os.path.abspath(cwd) != os.path.abspath(project):
        where = f"{os.path.basename(project.rstrip('/'))}/…/{where}"

    left = [c("36", f"[{model}]"), where]
    branch = git_info(cwd, session)
    if branch:
        left.append(c("35", branch))

    right = []
    pct = get(data, "context_window.used_percentage")
    if pct is not None:
        right.append("ctx " + bar(float(pct)))
    cost = get(data, "cost.total_cost_usd")
    if cost is not None:
        right.append(f"${float(cost):.2f}")
    five_h = get(data, "rate_limits.five_hour.used_percentage")
    if five_h is not None:
        right.append(c("31" if float(five_h) >= 80 else "2", f"5h {float(five_h):.0f}%"))

    sep = c("2", " | ")
    if os.environ.get("STATUSLINE_TWO_LINES") == "1":
        print(sep.join(left))
        if right:
            print(sep.join(right))
    else:
        print(sep.join(left + right))


if __name__ == "__main__":
    try:
        main()
    except Exception:  # a status line must never show a traceback
        print("[statusline error]")

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc