Skip to content

ludothegreat/usage

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

39 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

usage

Composable status line for Claude Code. A single Python script reads Claude Code's render payload from stdin and prints one line of ANSI to stdout. Pick the pieces you want, in the order you want them, and toggle them on and off with a one-liner.

How it works

Claude Code pipes a JSON payload to the status-line command after every assistant message, permission change, or vim toggle. statusline.py reads an ordered list of segments from ./active_segments, calls each segment function against the payload, and joins the non-empty results with a dim separator.

  • A segment is a small function that renders one piece of information (model, cwd, cost, clock, ...).
  • A preset is a named, canonical list of segments (e.g. default, minimal, clock).

You can start from a preset and tweak it with --enable / --disable, or set a full custom list with --set. If a segment crashes, the dispatcher replaces it with <segname?> in red so the rest of the bar keeps working.

An example composed bar

composed bar showing cwd+branch, tokens, 5h/7d rate limits, context, lines, duration, 200k+ badge

python3 statusline.py --set cwd_branch_dirty,tokens,rate_limits_bars,context_bar,lines,duration,exceeds_200k

Eight segments in one line: current working directory with dirty-aware branch, total tokens, both rate-limit bars with reset times, a context-usage indicator, lines added/removed, session duration, and the [200k+] pricing-tier badge lit up because this session is past the 200k-input mark. Every segment is documented individually below — this is just the assembled view.

Requirements

  • Python 3.11 or newer
  • No runtime dependencies (standard library only)
  • git on $PATH if you use the dirty / cwd_branch_dirty segments

Install

Clone somewhere permanent. The path gets baked into your Claude Code settings.

git clone <repo-url> ~/code/usage

Then edit ~/.claude/settings.json (create it if it doesn't exist) and add:

{
  "statusLine": {
    "type": "command",
    "command": "python3 /absolute/path/to/usage/statusline.py",
    "padding": 1
  }
}

Use the absolute path wherever you cloned. The next render event will pick up the default preset.

Segments

Run python3 statusline.py --list any time to see this with the currently-active ones marked *.

Name Shows
model Model name in bold orange.
cwd Working directory, tilde-compressed, truncated at 40 chars.
cwd_short Working directory basename only.
cwd_branch cwd followed by git branch when in a repo.
cwd_branch_dirty cwd + branch with * suffix when uncommitted changes exist.
branch Git branch name alone.
dirty Orange * when uncommitted changes exist, empty otherwise.
cost Session cost in USD, four decimals.
tokens Total tokens (input + output) in k / M form.
cost_tokens Cost and tokens in a single segment.
rate_5h 5-hour rate-limit usage percentage.
rate_7d Weekly rate-limit usage and reset time.
rate_limits 5-hour and weekly usage, compact.
rate_limits_bars 5-hour and weekly usage with bars and reset times.
context Context-window usage percentage.
context_bar 8-cell context bar with percentage only (no token counts).
context_bar_long 8-cell context bar plus raw input/output/max token counts.
context_bar_short 8-cell context bar with compact k/M tokens (←in/out→ max).
lines Lines added/removed during the session.
duration Elapsed session time.
api_time Time spent waiting on the API.
clock Current wall time and day abbreviation.
worktree Badge for ctx.worktree; empty when not launched from a worktree.
exceeds_200k Red [200k+] warning; empty unless the context has exceeded it.

Presets

Run python3 statusline.py --presets to see these as shortcuts you can pass to --set.

Preset Segments
default model, cwd_branch, cost_tokens, rate_limits, context, lines
minimal model, cwd_branch
rate_limits rate_limits_bars, cost
context_bar model, context_bar, exceeds_200k
session_timer model, cwd, duration, api_time, cost
clock model, cwd, clock
worktree model, cwd_branch, worktree
usage_budget model, cost, rate_limits
dirty_tree model, cwd_branch_dirty, cost

Configuring your status line

python3 statusline.py --show                       # print the active segment list
python3 statusline.py --list                       # all segments, active marked with *
python3 statusline.py --presets                    # all presets and what they expand to
python3 statusline.py --preset default             # apply a preset
python3 statusline.py --set model,cost,clock       # set a custom segment list
python3 statusline.py --enable worktree            # append a segment to the active list
python3 statusline.py --disable lines              # remove a segment from the active list
python3 statusline.py --sample                     # render every preset with a sample payload

Several preset names double as segment names (rate_limits, context_bar, clock, worktree). The two commands disambiguate cleanly: --preset <name> for presets, --set <name> for a single segment or a comma-separated segment list.

The active segment list lives in ./active_segments next to the script, one name per line. You can also edit it directly.

Resolution order (first match wins):

  1. STATUSLINE_PLUGIN environment variable (treated as a preset name)
  2. ./active_segments file
  3. Built-in default preset

One-shot override that doesn't touch the saved selection:

STATUSLINE_PLUGIN=context_bar claude

Wall-clock ticking (for the clock segment)

Most segments re-render only when Claude Code sends a payload. clock is the exception — to update on a schedule, add refreshInterval (milliseconds) to your statusLine block:

{
  "statusLine": {
    "type": "command",
    "command": "python3 /absolute/path/to/usage/statusline.py",
    "padding": 1,
    "refreshInterval": 30000
  }
}

Writing a segment

A segment is Callable[[dict], str]. Return a rendered string or "" to be skipped (no separator gets drawn on either side). Open statusline.py, write the function, and register it in SEGMENTS plus SEGMENT_DOCS:

def segment_my_bit(ctx: dict) -> str:
    model = (ctx.get("model") or {}).get("display_name") or "?"
    return f"{model} hello"


SEGMENTS = {
    ...
    "my_bit": segment_my_bit,
}

SEGMENT_DOCS = {
    ...
    "my_bit": "Says hello to the current model.",
}

Then verify and activate:

python3 statusline.py --sample
python3 statusline.py --enable my_bit

Contract

  • Return exactly one line; empty string to skip. ANSI and OSC-8 hyperlinks are fine.
  • Stay under ~200 ms — the script runs on every render event.
  • No subprocesses, no network. Cheap local file reads are fine (the built-in git_branch reads .git/HEAD directly). git_dirty is the single exception: one git status call with a 150 ms timeout, fails closed.
  • Defend against missing fields. Optional sections like rate_limits, vim, agent, worktree, and session_name are absent, not null. Numeric fields like context_window.used_percentage can be null early in a session. Use (ctx.get("x") or {}).get("y") or default rather than bracket access.

Helpers

The top of statusline.py has reusable helpers. Prefer them over rolling your own.

Helper Purpose
safe_cwd(ctx) Home-directory-aware path string.
truncate_cwd(path, max_len=40) Middle-ellipsis shortener for long paths.
git_branch(cwd) Branch name, short hash on detached HEAD, or None. Handles worktrees.
git_dirty(cwd) True / False / None. Uses git status with a 150 ms timeout.
pct_color(p) Green / orange / red bands at 50% and 80%.
bar(pct, width) Colored progress bar.
human_tokens(n) Token counts as k / M.
fmt_reset(ts) Unix epoch to a Thu 2pm-style label in local time.
fmt_duration(ms) Milliseconds to 1h 23m / 45m / 12s.

Segment walkthrough

Each segment shown in isolation — a screenshot of what it looks like live, the --set command to try it, and a collapsible with what it does under the hood.

1. model

model segment active

python3 statusline.py --set model
How it works

What it shows: The current model's display name.

Source: statusline.py:214

def segment_model(ctx: dict) -> str:
    model = (ctx.get("model") or {}).get("display_name") or "?"
    return f"{BOLD}{ORANGE}{model}{RESET}"

What it reads from the payload: ctx.model.display_name. Claude Code always sends a model object, but display_name could theoretically be missing — the or "?" means you'd see a bold-orange ? instead of a crash.

Why bold orange: Orange (256-color 208) is the accent color in this palette — it stands in for yellow, which this repo doesn't use. Bold + orange makes the model name pop as the "primary label" of the bar regardless of what else is beside it.

Edge cases:

  • "model": null in the payload → ?.
  • display_name is an empty string → same fallback (empty string is falsy).

Typical use: Almost always first. Every preset starts with model except rate_limits, which is focused purely on usage numbers.

2. cwd

cwd segment active

python3 statusline.py --set cwd
How it works

What it shows: The directory Claude Code was launched from, home-folder-compressed, and truncated to 40 characters if long.

Source: statusline.py:219

def segment_cwd(ctx: dict) -> str:
    return f"{CYAN}{truncate_cwd(safe_cwd(ctx))}{RESET}"

What it reads from the payload: ctx.cwd. Passes through two helpers:

  1. safe_cwd() — collapses $HOME to ~ (so /home/you/code/foo becomes ~/code/foo), and falls back to ? if ctx.cwd is missing or empty.
  2. truncate_cwd(path, max_len=40) — if the path is longer than 40 characters, collapses the middle with while keeping the first segment (the anchor) and the basename. A deep path like /Users/…/LambdaNode still tells you where you are.

Why cyan: The path is secondary information — you usually know where you are. Cyan is quiet enough not to fight the bold-orange model name but still legible on dark backgrounds. It's the only cool-palette color in the repo, which makes "location" visually distinct from everything else.

Edge cases:

  • ctx.cwd absent, null, or ""?.
  • Path exactly matches $HOME~ (no trailing slash).
  • Path exceeds 40 chars → middle-ellipsis truncation that preserves the first segment and the basename.
  • Absolute paths always retain exactly one leading / (regression-tested).

Typical use: Alone when you want just the path with no branch or dirty decoration. If you're usually in a git repo, cwd_branch is a better fit.

3. cwd_short

cwd_short segment active

python3 statusline.py --set cwd_short
How it works

What it shows: Only the last segment of the current path — just the directory name, no parents. /hoard/lab/usage renders as usage.

Source: statusline.py:223

def segment_cwd_short(ctx: dict) -> str:
    full = safe_cwd(ctx)
    name = Path(full).name or full
    return f"{CYAN}{name}{RESET}"

What it reads from the payload: ctx.cwd, through safe_cwd() (same home-folder compression as cwd), then Path(full).name extracts the basename.

The or full fallback matters: Path("/").name returns an empty string. At the filesystem root, Path has no basename to give back, so the fallback keeps it from rendering blank. Same for Path("~").name.

Why cyan (same as cwd): Both segments are "location" information, so they share the visual language.

Edge cases:

  • ctx.cwd absent → ?.
  • At filesystem root // rendered.
  • At home directory $HOME~ rendered.
  • Trailing-slash paths like /foo/bar/bar (Python's Path.name strips trailing separators).

When to use vs. cwd: cwd_short is for when you want the current project name but don't care about the full path. cwd gives you the full context. Mutually exclusive — pick one.

4. cwd_branch

cwd_branch segment active

python3 statusline.py --set cwd_branch
How it works

What it shows: The cwd plus the git branch when you're inside a repo, joined by a dim middot. Outside a repo, falls back to just the cwd.

Source: statusline.py:231

def segment_cwd_branch(ctx: dict) -> str:
    cwd = truncate_cwd(safe_cwd(ctx))
    branch = git_branch(ctx.get("cwd"))
    out = f"{CYAN}{cwd}{RESET}"
    if branch:
        out += f" {DIM}·{RESET} {GREEN}{branch}{RESET}"
    return out

What it reads from the payload: ctx.cwd. The path runs through safe_cwd and truncate_cwd like plain cwd; the branch comes from the git_branch() helper.

How git_branch works (the interesting bit): Walks up from ctx.cwd looking for a .git/ directory or a .git file (worktrees use the file form — it points at the real gitdir). Reads .git/HEAD directly — no git subprocess:

  • HEAD contains ref: refs/heads/<name> → returns <name>. Normal on-a-branch case.
  • HEAD contains a raw commit SHA (detached HEAD) → returns the first 7 characters.
  • No .git found walking up, or unparseable HEAD → returns None.

All of that is pure Path.read_text(). Stays under a millisecond even on deep trees.

Why dim middot between cwd and branch: A piped is what the dispatcher uses between segments. Within a single segment, a dim · signals "these two things are related" rather than "these are separate pieces." Matches how most terminal prompts handle the pairing.

Why green for the branch: Green is "contextual, positive" in this palette — same color used for +lines added and $cost. The branch name is information you want in the "everything is fine" register.

Edge cases:

  • Not in a repo → just the cwd, no dot, no branch.
  • Detached HEAD → branch displays as a 7-char short hash.
  • Git worktree (the git worktree add kind) → walks the pointer file in .git to the real gitdir and reads HEAD there.
  • Unreadable .git/HEAD (perm error) → just the cwd; no crash, no visible error.

Typical use: The default preset's cwd segment. If you also care about whether the tree is dirty, see cwd_branch_dirty.

5. cwd_branch_dirty

cwd_branch_dirty segment active

python3 statusline.py --set cwd_branch_dirty
How it works

What it shows: cwd and branch like cwd_branch, plus a dirty-tree indicator. Three visual states for the branch:

  • Clean tree → branch in green (same as cwd_branch).
  • Dirty tree → branch in orange with a * suffix (pictured above).
  • Unknown → branch in dim gray (not in a repo, git status timed out, or git not on PATH).

Source: statusline.py:239

def segment_cwd_branch_dirty(ctx: dict) -> str:
    cwd = truncate_cwd(safe_cwd(ctx))
    branch = git_branch(ctx.get("cwd"))
    out = f"{CYAN}{cwd}{RESET}"
    if branch:
        dirty = git_dirty(ctx.get("cwd"))
        if dirty is True:
            label = f"{ORANGE}{branch}*{RESET}"
        elif dirty is False:
            label = f"{GREEN}{branch}{RESET}"
        else:
            label = f"{DIM}{branch}{RESET}"
        out += f" {DIM}·{RESET} {label}"
    return out

What it reads from the payload: ctx.cwd. Same as cwd_branch for the path and branch; dirtiness comes from the git_dirty() helper.

How git_dirty works — and why it's the one exception to the plugin contract:

The rest of this repo avoids subprocesses. git_branch reads .git/HEAD directly because walking a file is cheap and deterministic. Detecting "is the working tree dirty?" in pure Python means either parsing git's binary .git/index format (complex, fragile across git versions) or stat()-ing every tracked file and comparing against the index (expensive on large repos).

So this helper shells out — once per render, with a hard 150 ms timeout:

def git_dirty(cwd: str | None) -> bool | None:
    if not cwd:
        return None
    try:
        result = subprocess.run(
            ["git", "-C", cwd, "status", "--porcelain=v1"],
            capture_output=True, text=True, timeout=0.15,
        )
    except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
        return None
    if result.returncode != 0:
        return None
    return bool(result.stdout.strip())

The design is fail closed. Any error (timeout, missing git, non-zero exit, permission problem) returns None, which renders the branch in dim gray. The status line never blocks waiting on git, and never shows stale or misleading state.

Why orange-with-asterisk for dirty: Orange is the palette's "heads up, but not a crisis" color. A dirty tree is normal during work; you just want to notice it. The * echoes git's own conventional marker from git log --oneline and prompt tools like oh-my-zsh.

Why dim for unknown: You want the branch name visible either way, but dim communicates "I can't tell you the status right now" without false confidence.

Edge cases:

  • Not in a repo → just the cwd, no branch, no dirty indicator (same as cwd_branch).
  • In a repo but git not on PATH → branch shown dim (unknown).
  • In a repo but git status takes >150 ms → branch shown dim.
  • Detached HEAD in a dirty tree → short hash with * suffix in orange.

Typical use: Replaces cwd_branch when you want the dirty flag. The dirty_tree preset uses exactly this segment.

6. branch

On main:

branch segment on main

On a feature branch with a slash in the name:

branch segment on feature/plugins

python3 statusline.py --set branch
How it works

What it shows: Just the git branch name in green, no surrounding path or separator. Outside a repo it renders nothing — the compositor skips empty segments entirely.

Source: statusline.py:227

def segment_branch(ctx: dict) -> str:
    branch = git_branch(ctx.get("cwd"))
    return f"{GREEN}{branch}{RESET}" if branch else ""

What it reads from the payload: ctx.cwd, through git_branch(). Same helper as cwd_branch / cwd_branch_dirty — walks up looking for .git, reads HEAD, returns the branch name or a 7-char hash on detached HEAD. Slash-containing branches like feature/plugins render verbatim.

Why this exists as its own segment: You might want the branch separated from the cwd by the dispatcher's pipe separator () instead of the tight · used in cwd_branch. Example composition:

python3 statusline.py --set cwd,branch,cost

Renders like ~/code/foo │ main │ $0.04, with the branch as its own visual unit rather than tightly joined to the path. Most people prefer the tight coupling of cwd_branch, which is why the presets default to that — but branch is here if you disagree.

Empty-segment behavior: Outside a repo this returns "". The dispatcher skips empty segments entirely — no stray separator, no gap, no placeholder, the rest of the bar closes up around the hole. This is the first segment in the walkthrough that exercises the "skip empty" path; every subsequent segment that can have no data (dirty, worktree, exceeds_200k, duration, api_time, rate_5h, rate_7d) uses the same pattern.

Edge cases:

  • Not in a repo → empty, skipped.
  • Detached HEAD → 7-char short hash in green.
  • .git/HEAD unreadable → empty, skipped.
  • Branch name with slashes (feature/plugins, release/v2) renders verbatim — no special handling.

Typical use: Rarely alone. Usually combined with cwd when you want the two visually separate, or in custom compositions where you want branch info isolated from the cwd. For the common "path and branch together" want, use cwd_branch.

7. dirty

dirty segment active on a dirty tree

python3 statusline.py --set dirty
How it works

What it shows: Orange * when the working tree has uncommitted changes. Empty in every other case — not in a repo, clean tree, git status timed out, or git unavailable.

Source: statusline.py, segment_dirty

def segment_dirty(ctx: dict) -> str:
    if not git_branch(ctx.get("cwd")):
        return ""
    dirty = git_dirty(ctx.get("cwd"))
    if dirty is True:
        return f"{ORANGE}*{RESET}"
    return ""

Why the git_branch() guard at the top: We only want to run the comparatively-expensive git_dirty() subprocess after confirming we're inside a repo. If git_branch() returns None, we skip the subprocess entirely and return empty. Cheap fast-path.

What it reads from the payload: ctx.cwd. Delegates to git_branch and git_dirty — same code path as cwd_branch_dirty, just the indicator alone.

Why this exists as its own segment: Composition. If you want the branch in one position and the dirty flag in another, you can split them:

python3 statusline.py --set model,branch,cwd,dirty,cost

That gives you a bar where the * shows up detached from the branch name. Or you might want the asterisk far-right as a subtle "don't forget" nudge:

python3 statusline.py --set model,cwd_branch,cost,dirty

Most people want the asterisk attached to the branch, which is what cwd_branch_dirty gives you. dirty is here for the times you want it somewhere else.

Empty-segment behavior: When clean, returns "" and gets skipped by the dispatcher — no stray separator. Same for "not in a repo" and "unknown." This matters because clean is the most common state; you don't want a trailing │ │ in your bar when nothing's wrong.

Edge cases:

  • Not in a repo → empty.
  • Clean tree → empty.
  • git unavailable / git status timeout → empty (fail closed, same as cwd_branch_dirty).
  • Dirty tree → orange *.

Typical use: Composing custom bars where you want granular control over where the dirty flag lives. If you just want "branch + dirty together," cwd_branch_dirty is the one-shot.

8. cost

cost segment showing $37.9318

python3 statusline.py --set cost
How it works

What it shows: Session cost in USD with four decimal places, in green. Grows across the session as Claude Code sends updated payloads; resets only when a new session starts.

Source: statusline.py, segment_cost

def segment_cost(ctx: dict) -> str:
    cost = (ctx.get("cost") or {}).get("total_cost_usd") or 0.0
    return f"{GREEN}${cost:.4f}{RESET}"

What it reads from the payload: ctx.cost.total_cost_usd. This is the running total of what the current session has spent.

The or 0.0 guard matters: Early in a session, before the first API call, total_cost_usd can be absent or present-but-null. The pattern (ctx.get("cost") or {}).get("total_cost_usd") or 0.0 handles three cases cleanly:

  1. ctx.cost missing → falls back to {}, .get("total_cost_usd") returns None, or 0.0 kicks in → $0.0000.
  2. ctx.cost present but total_cost_usd missing → .get(...) returns None, or 0.0$0.0000.
  3. ctx.cost.total_cost_usd is null.get(...) returns None, or 0.0$0.0000.

Without that guard, f"${None:.4f}" would crash the render and you'd see a red statusline: segment_cost crashed line.

Why four decimals: Claude sessions commonly hit costs like $0.0042 or $0.0183. Rounding to two decimals would show most of those as $0.00, which is useless for watching a session's cost rise. Four decimals gives you visible tenth-of-a-cent resolution while still reading cleanly for larger sums.

Why green: In this palette, green is the "normal, positive" color — branch names, cost, +lines added. Cost is money you chose to spend, not a warning. It only becomes a problem if the number grows faster than you expected; for that context, pair with rate_5h / rate_7d.

Edge cases:

  • Fresh session, no API calls yet → $0.0000.
  • Cost under one cent → $0.0012, still visibly non-zero.
  • Large session (hours of deep work, like the $37.9318 pictured) → renders the same way, no truncation.
  • total_cost_usd null or absent → $0.0000.

Typical use: Almost always paired with either tokens (how much you've used) or rate_limits (how close you are to a ceiling). The cost_tokens segment combines cost + tokens compactly; usage_budget pairs cost with the rate-limit percentages. Pure cost alone is for when you just want the money number without the noise.

9. tokens

tokens segment showing 227.2k tok

python3 statusline.py --set tokens
How it works

What it shows: Total tokens (input + output) used in the session, formatted as k or M, in dim gray — e.g. 19.8k tok or 1.20M tok.

Source: statusline.py, segment_tokens

def segment_tokens(ctx: dict) -> str:
    cw = ctx.get("context_window") or {}
    total = (cw.get("total_input_tokens") or 0) + (cw.get("total_output_tokens") or 0)
    return f"{DIM}{human_tokens(total)} tok{RESET}"

What it reads from the payload: Two fields under ctx.context_windowtotal_input_tokens and total_output_tokens, summed. Running totals for the session, not per-message counts.

Why (x or 0) + (y or 0) instead of .get(..., 0): Same reason as cost — Claude Code can send these fields as null early in a session rather than omitting them. .get(key, 0) returns None when the value is explicitly null. The or 0 pattern handles both null and missing uniformly. None + None would crash; 0 + 0 gives 0 tok.

How human_tokens formats the number:

def human_tokens(n: int) -> str:
    if n < 1000:
        return str(n)
    if n < 1_000_000:
        return f"{n / 1000:.1f}k"
    return f"{n / 1_000_000:.2f}M"

Three bands:

  • < 1000 → raw integer, e.g. 427.
  • 1000 – 999_999 → one decimal + k, e.g. 19.8k or 227.2k.
  • ≥ 1_000_000 → two decimals + M, e.g. 1.20M.

The decimal places differ on purpose: at the k scale, one decimal is enough resolution (the difference between 19.8k and 19.9k is 100 tokens, which is noise). At the M scale, two decimals gives 10k resolution — the threshold where you start caring about individual turn costs.

Why dim: Token count is correlated information — cost and context-window percentage already tell you most of what you need. Dim keeps it readable but quiet so it doesn't steal attention from the segments beside it. This is also why the cost_tokens combined segment renders the cost in bright green and the tokens in dim: visual hierarchy.

Why the literal " tok" suffix: Without it, 19.8k reads as "nineteen thousand what?" — could be lines, dollars-in-millicents, anything. 19.8k tok makes the unit unambiguous at a glance.

Edge cases:

  • Fresh session → 0 tok.
  • Only input tokens counted (no output yet) → still renders, e.g. 847 tok.
  • Either field null or absent → treated as zero, sum still works.

Typical use: Rarely alone. cost_tokens is the usual way to see this data (paired with the dollar figure). Pure tokens is for a minimal bar that tracks usage volume without the cost implication.

10. cost_tokens

cost_tokens segment showing $39.9481 · 231.5k tok

python3 statusline.py --set cost_tokens
How it works

What it shows: Cost and tokens combined in one segment. Bright green dollar figure, dim middot, dim token count.

Source: statusline.py, segment_cost_tokens

def segment_cost_tokens(ctx: dict) -> str:
    cost = (ctx.get("cost") or {}).get("total_cost_usd") or 0.0
    cw = ctx.get("context_window") or {}
    total = (cw.get("total_input_tokens") or 0) + (cw.get("total_output_tokens") or 0)
    return f"{GREEN}${cost:.4f}{RESET} {DIM}· {human_tokens(total)} tok{RESET}"

Same payload reads, same fallbacks, same formatting rules as cost + tokens individually — spliced together in one output.

Why this exists as its own segment rather than letting you compose cost + tokens: Visual grouping. If you put them as separate segments:

python3 statusline.py --set cost,tokens

You get $37.9318 │ 227.2k tok — two top-level segments joined by the bold pipe separator. They read as distinct pieces of data, which is misleading because they're the same information in different units (money you spent ≈ tokens you used).

cost_tokens uses a dim · instead of a , signaling "these belong together" — same pattern as cwd_branch. The cost is loud, the tokens are dim; the dim middot says "same unit of meaning, two views of it."

Visual hierarchy (green + dim, not green + green): If both halves were bright green, your eye would bounce between them not knowing which to read first. Green on the cost says "here's the number that matters." Dim on the tokens says "here's the supporting detail." You can glance at the cost and stop; you can focus on the tokens when you want the second view.

Why different precisions (.4f for cost, k/M for tokens): Different scales. Cost is tens of dollars at most per session — four decimals give cent-and-a-half resolution. Tokens hit hundreds of thousands to millions — k/M is the only sane way to render them. Matching the formats would make one or the other unreadable.

Edge cases (same as cost and tokens combined):

  • Fresh session → $0.0000 · 0 tok.
  • Either sub-field null or absent → both fall back to zero; segment still renders.
  • Very long session → $12.4317 · 1.20M tok, both scale gracefully.

Typical use: The default preset's cost-and-usage segment. More common than either cost or tokens alone. If you care about both numbers (most people do), this is the right choice; if you only care about one, use the solo version.

11. rate_5h

rate_5h segment showing 5h 1%

python3 statusline.py --set rate_5h
How it works

What it shows: 5-hour rate-limit usage as a single compact item: 5h X%. The percentage is color-graded — green below 50%, orange 50–80%, red above.

Source: statusline.py, segment_rate_5h

def segment_rate_5h(ctx: dict) -> str:
    rl = ctx.get("rate_limits") or {}
    five = (rl.get("five_hour") or {}).get("used_percentage")
    if five is None:
        return ""
    return f"5h {pct_color(five)}{five:.0f}%{RESET}"

What it reads from the payload: ctx.rate_limits.five_hour.used_percentage. Claude tracks this server-side and sends it once the session has made API calls.

The explicit if five is None check: This is the first segment in the walkthrough that actually skips itself based on payload content. Two reasons the percentage can be missing:

  1. Absent section → ctx.rate_limits simply isn't in the payload (you haven't hit any rate-counted endpoint yet). (ctx.get("rate_limits") or {}) handles that — returns {}, and .get("five_hour") on an empty dict is None.
  2. Present but empty → rate_limits.five_hour can be None early in a session. (rl.get("five_hour") or {}).get("used_percentage") handles that too.

Either way, five is None → return "" → segment is skipped by the dispatcher, no stray 5h -% placeholder.

How pct_color bands the value:

def pct_color(p: float | int | None) -> str:
    if p is None:
        return GRAY
    if p < 50:
        return GREEN
    if p < 80:
        return ORANGE
    return RED

Three visual states:

  • < 50% → green (you're fine).
  • 50–79% → orange (slow down).
  • ≥ 80% → red (near the ceiling).

The 50/80 thresholds aren't magic — they match common "warning / critical" conventions from system monitoring. Tune them in pct_color if you want softer or harder thresholds.

Why .0f (no decimals): Rate-limit percentage is an approximation. Showing 42.37% implies precision the number doesn't carry; 42% is honest and fits cleaner in the bar.

Why no reset time here: That's the responsibility of rate_7d (where the reset time is actually useful — weekly resets are far enough away that you care when) and the combined rate_limits segment. The 5-hour window resets too frequently for the reset time to be worth the screen space — by the time you read it, it's nearly meaningless.

Edge cases:

  • No rate-limit data in payload → segment empty, skipped.
  • Percentage is null (present but empty) → segment empty, skipped.
  • used_percentage is 0 → renders 5h 0% in green. Zero is data, not "no data."
  • 100% → renders 5h 100% in red.

Typical use: In a custom bar where you want just the 5-hour window and not the weekly. If you want both, rate_limits (compact) or rate_limits_bars (with progress bars) combine them.

12. rate_7d

rate_7d segment showing 7d 47% (Thu 2pm)

python3 statusline.py --set rate_7d
How it works

What it shows: Weekly rate-limit usage plus the window's reset time — e.g. 7d 47% (Thu 2pm). Same green/orange/red color bands on the percentage as rate_5h; reset time in dim gray inside parentheses.

Source: statusline.py, segment_rate_7d

def segment_rate_7d(ctx: dict) -> str:
    rl = ctx.get("rate_limits") or {}
    seven = (rl.get("seven_day") or {}).get("used_percentage")
    if seven is None:
        return ""
    out = f"7d {pct_color(seven)}{seven:.0f}%{RESET}"
    reset = fmt_reset((rl.get("seven_day") or {}).get("resets_at"))
    if reset:
        out += f" {DIM}({reset}){RESET}"
    return out

What it reads from the payload: Two fields under ctx.rate_limits.seven_day:

  1. used_percentage — same shape and color bands as the 5-hour version.
  2. resets_at — a Unix epoch timestamp (seconds) indicating when the weekly window rolls over.

Why the reset time matters for the 7-day window (but not 5-hour): A 5-hour window resets ~5× per day — knowing the exact moment doesn't change behavior. A 7-day window resets once a week. If you're at 80% and the reset is 30 hours away, that's very different from 80% with 6 days to go. The reset label turns the percentage into a decision.

How fmt_reset formats the epoch:

def fmt_reset(ts: int | float | None) -> str:
    if not ts:
        return ""
    try:
        dt = datetime.fromtimestamp(int(ts)).astimezone()
    except (OSError, OverflowError, ValueError):
        return ""
    hour = dt.strftime("%-I").lower()
    ampm = dt.strftime("%p").lower()
    minutes = dt.minute
    time_part = f"{hour}{ampm}" if minutes == 0 else f"{hour}:{minutes:02d}{ampm}"
    return f"{dt.strftime('%a')} {time_part}"

Worth unpacking:

  • datetime.fromtimestamp(int(ts)).astimezone() — converts epoch seconds to a local-time datetime, tagged with your system timezone. .astimezone() is the important part; without it, formatting uses naïve time which would be UTC-wrong for most readers.
  • %-I — hours in 12-hour form without leading zero. %I gives 02; %-I gives 2. Makes 2pm read naturally instead of 02pm.
  • Minute conditionalhour alone when on the hour (2pm), hour:MM otherwise (2:30pm). Cleaner in the common case where reset times fall on the hour.
  • dt.strftime('%a') — weekday abbreviation (Thu, Mon).

So epoch 1776970800 renders as Thu 2pm in Central Time, Fri 5am in Tokyo — each user sees the reset moment in their own timezone, no math required.

Fallbacks:

  • ts is None or 0 → returns "", the (... reset) suffix is omitted entirely.
  • fromtimestamp raises on extreme values (32-bit overflow, etc.) → returns "".

In both cases you still see 7d X% — just without the reset annotation.

Why dim on the reset, bright on the percentage: Same visual-hierarchy principle as cost_tokens. The percentage is the number that matters; the reset time is context.

Edge cases:

  • No rate_limits.seven_day → segment empty, skipped.
  • used_percentage present but resets_at missing → percentage renders, no (Thu 2pm) suffix.
  • Reset exactly on the hour → (Thu 2pm), not (Thu 2:00pm).
  • Reset at 30 minutes past → (Thu 2:30pm).

Typical use: In a custom bar when you want the weekly window tracked but not the 5-hour. More commonly combined with rate_5h via the rate_limits or rate_limits_bars segments.

13. rate_limits

rate_limits segment showing 5h 3% · 7d 47% (Thu 2pm)

python3 statusline.py --set rate_limits
How it works

What it shows: Both rate-limit windows compacted into one segment — 5h X% · 7d Y% (Thu 2pm). Same percentages as the standalone rate_5h / rate_7d, joined with a dim middot; the reset label attaches only to the 7-day portion.

Gotcha: rate_limits is also a preset name. Use --set rate_limits for the segment alone; use --preset rate_limits to apply the two-segment preset (rate_limits_bars, cost). Same disambiguation applies to context_bar, clock, and worktree.

Source: statusline.py, segment_rate_limits

def segment_rate_limits(ctx: dict) -> str:
    rl = ctx.get("rate_limits") or {}
    five = (rl.get("five_hour") or {}).get("used_percentage")
    seven = (rl.get("seven_day") or {}).get("used_percentage")
    parts: list[str] = []
    if five is not None:
        parts.append(f"5h {pct_color(five)}{five:.0f}%{RESET}")
    if seven is not None:
        seg = f"7d {pct_color(seven)}{seven:.0f}%{RESET}"
        reset = fmt_reset((rl.get("seven_day") or {}).get("resets_at"))
        if reset:
            seg += f" {DIM}({reset}){RESET}"
        parts.append(seg)
    return f" {DIM}·{RESET} ".join(parts)

What it shows: The merge of rate_5h and rate_7d into one visually-grouped segment. Each component is independent — either can be absent without breaking the other.

Why one segment instead of composing rate_5h,rate_7d: Same reasoning as cost_tokens — these two numbers are the same kind of information (rate-limit usage) in different time windows. Composing them as separate top-level segments:

python3 statusline.py --set rate_5h,rate_7d

...gives you 5h 3% │ 7d 47% (Thu 2pm) — two top-level segments joined by the bold , reading as if they're unrelated pieces of data. The rate_limits segment uses a dim · instead, signaling "same kind of thing, two views." Matches the cwd · branch and cost · tokens pattern.

Why only the 7-day gets the reset label: Same rationale as the standalone rate_7d segment — the 5-hour window resets too often for the reset time to be useful. Putting (2pm) next to the 5-hour number would crowd the bar without telling you anything you care about.

Independent visibility — the parts list is the key: The function builds a parts list and only appends entries for data that's present. If only the 5-hour data is in the payload, you get 5h 3%. If only the 7-day data is present, you get 7d 47% (Thu 2pm). If both are missing, parts stays empty and " · ".join(parts) returns "" — segment skipped entirely. The "gracefully degrades" pattern.

Edge cases:

  • Both windows missing → segment empty, skipped.
  • Only 5-hour → 5h X%, no trailing middot.
  • Only 7-day → 7d Y% (Thu 2pm).
  • 7-day used_percentage present but resets_at missing → 7d Y% without the paren suffix.

Typical use: The default preset's rate-tracking segment. More common than either rate_5h or rate_7d alone. For a richer view with progress bars, use rate_limits_bars.

14. rate_limits_bars

rate_limits_bars segment

python3 statusline.py --set rate_limits_bars
How it works

What it shows: The expanded rate-limits view — both windows with 8-character progress bars, percentages, and reset labels for each. Format: 5h [bar] X% (reset) │ 7d [bar] Y% (reset).

Source: statusline.py, segment_rate_limits_bars

def segment_rate_limits_bars(ctx: dict) -> str:
    rl = ctx.get("rate_limits") or {}
    five = (rl.get("five_hour") or {}).get("used_percentage")
    seven = (rl.get("seven_day") or {}).get("used_percentage")
    parts: list[str] = []
    if five is not None:
        seg = f"5h {bar(five, 8)} {pct_color(five)}{five:.0f}%{RESET}"
        reset = fmt_reset((rl.get("five_hour") or {}).get("resets_at"))
        if reset:
            seg += f" {DIM}({reset}){RESET}"
        parts.append(seg)
    if seven is not None:
        seg = f"7d {bar(seven, 8)} {pct_color(seven)}{seven:.0f}%{RESET}"
        reset = fmt_reset((rl.get("seven_day") or {}).get("resets_at"))
        if reset:
            seg += f" {DIM}({reset}){RESET}"
        parts.append(seg)
    return SEG_SEP.join(parts)

What's different from rate_limits (compact):

compact (rate_limits) bars (rate_limits_bars)
progress bar no yes (8-char)
5h reset label no yes (Sat 9pm)
7d reset label yes yes
joiner between 5h and 7d dim middot · bold pipe

Two design choices worth calling out:

Why both windows get reset labels here (but not in compact): The bars version is already space-heavy — one bar per window, two-digit percentage, color grading. At that size, adding the 5-hour reset label doesn't meaningfully crowd the bar, and it gives you something useful: 5h ______ 4% (Sat 9pm) anchors the "4%" to an endpoint in time. Is 4% good because the window is fresh, or am I about to hit a reset? The label answers. In the compact view there's no room for that answer, so we drop it.

Why bold pipe between 5h and 7d (instead of dim middot): Each sub-entry has a visible bar, which makes it feel like a complete unit on its own. Joining two "complete units" with a dim middot would understate the break; the bold pipe says "here's one visual unit, here's another." In the compact version each side is just 5h 4% — tiny — so pulling them tight with a dim middot reinforces they're the same category of thing.

How bar(pct, 8) renders:

def bar(pct: float | int | None, width: int = 12) -> str:
    if pct is None:
        return GRAY + ("·" * width) + RESET
    pct = max(0.0, min(100.0, float(pct)))
    filled = round(width * pct / 100)
    color = pct_color(pct)
    return color + ("█" * filled) + GRAY + ("░" * (width - filled)) + RESET

Standard filled/empty block bar, 8 cells wide here (context_bar uses 20). The filled portion uses the same color grading as the percentage — green/orange/red based on pct_color. The empty portion is dim gray. None input renders a fully-dim dotted bar as a placeholder.

Edge cases:

  • Both windows missing → segment empty, skipped.
  • pct is 0 → zero filled cells, all dim gray; bar frame still visible.
  • pct is 100 → all eight cells filled, red.
  • pct is a float like 42.37 → rounded to 3 filled cells (round(8 * 0.4237) = 3), displayed as 42%.

Typical use: The rate_limits preset's primary segment (combined with cost for the full "how close am I to trouble" view). Useful for long sessions where you want at-a-glance visual tracking rather than parsing percentages.

15. context

context segment showing ctx 41%

python3 statusline.py --set context
How it works

What it shows: ctx X% — the percentage of your current context window that's been consumed, color-graded green/orange/red.

Source: statusline.py, segment_context

def segment_context(ctx: dict) -> str:
    cw = ctx.get("context_window") or {}
    used = cw.get("used_percentage")
    used_str = f"{used}%" if used is not None else "-"
    return f"ctx {pct_color(used)}{used_str}{RESET}"

What it reads from the payload: ctx.context_window.used_percentage. The percentage of the session's context capacity filled by prior turns — how close you are to a compaction event.

Why this segment always renders (unlike rate_5h / rate_7d): Notice there's no if used is None: return "" here. Three reasons the context percentage matters even when null:

  1. Early in a session, used_percentage is often null — the session hasn't sent enough data to meter. But you still want to see ctx - in the bar so you know it's being tracked; the absence of the number is itself data.
  2. The segment always says "ctx", so you always know what's in that slot — never a mystery empty space.
  3. pct_color(None) returns gray, which visually communicates "I don't have this number yet" in exactly the way you'd want.

This is different from rate-limit segments, which hide entirely when there's no data — the difference being that rate limits are optional features (you might not be on a plan that has them), whereas the context window is always a constraint on any Claude session.

How the fallback renders:

  • used_percentage = 0ctx 0% in green
  • used_percentage = 41ctx 41% in green
  • used_percentage = 65ctx 65% in orange
  • used_percentage = 92ctx 92% in red
  • used_percentage = Nonectx - in gray

Why ctx and not context: Space. The bar is usually packed by the time you get to this segment. A three-letter label is the minimum that still reads as "context" rather than a random abbreviation.

Why no bar here (as in context_bar): Two reasons:

  1. context is for a quick-glance number, not a visualization. For the bar, use context_bar.
  2. A number tells you the trend faster than a bar once you know the scale. You glance at ctx 41% and know you have headroom; you glance at ctx 92% and know compaction is imminent. The bar is more useful for visual monitoring over time; the number is better for decision-making right now.

When context compaction happens: Claude Code starts compacting context around 85-90% by default. If used_percentage is in the red band (≥80%), you're approaching it. The segment's red color is a UX warning — maybe wrap up what you're doing before Claude needs to cut older turns.

Edge cases:

  • Fresh session, no API calls → used_percentage often nullctx - in gray.
  • used_percentage: 0ctx 0% in green (distinguishable from null).
  • After compaction → used_percentage resets lower.
  • context_window section missing entirely → same as null → ctx -.

Typical use: The default preset's context-tracking segment. Most sessions want the number, not the bar, because context is background info — you want to notice when it's high, but you don't want a bar for something you're mostly ignoring. For deep research or long debugging sessions where context-watching is the main job, context_bar gives you the richer view.

16. context_bar

context_bar segment

python3 statusline.py --set context_bar
How it works

What it shows: ctx label + 8-cell progress bar + percentage — no token counts, no suffix. Middle of the context family: more visual than context, lighter weight than context_bar_long or context_bar_short. Bar width matches rate_limits_bars so the two align visually when both segments are active. The ctx label is there because — unlike context_bar_long / _short which carry (in/out/max) or ←...→ giveaways — a bare bar+percentage alone could be mistaken for any other metric.

Gotcha: context_bar is both a segment and a preset. Use --set context_bar for the segment alone (just bar + percentage). Use --preset context_bar to apply the preset, which expands to model, context_bar_long, exceeds_200k — note the preset uses the long variant so it includes the raw counts by default.

Source: statusline.py, segment_context_bar

def segment_context_bar(ctx: dict) -> str:
    cw = ctx.get("context_window") or {}
    used = cw.get("used_percentage")
    used_str = f"{used}%" if used is not None else "-"
    return f"ctx {bar(used, 8)} {pct_color(used)}{used_str}{RESET}"

What it reads from the payload: Just ctx.context_window.used_percentage. The token-count fields aren't touched here — the long variant handles those.

Why 8 cells for the bar: Matches rate_limits_bars and context_bar_short. Each cell represents 12.5% — coarser than a 20-cell bar's 5% per cell, but enough resolution to see "empty / quarter / half / near-full" at a glance. The visual consistency with the other 8-cell bars is worth more than the extra precision of a wider bar, especially in a busy line.

When to use this over the alternatives:

you want segment
just the number context (ctx 41%)
bar + number, nothing else context_bar (this one)
bar + number + full raw counts context_bar_long
bar + number + compact k/M counts context_bar_short

context_bar is the choice when you want a visual usage indicator but don't need the absolute token numbers — you trust the percentage to tell you what you need. In a busy bar this saves ~40 characters versus the long variant.

Edge cases:

  • used_percentage is null → bar renders as 20 dim dots (····················), - for the percentage.
  • Fresh session → same dotted bar until Claude Code reports usage.
  • context_window section missing entirely → same as null, segment still renders (the "always visible" design shared with context).

Typical use: Minimalist bars where you want context awareness but don't want the long in/out/max triple crowding things.

17. lines

lines segment showing +3552/-464

python3 statusline.py --set lines
How it works

What it shows: +N/-M — the number of lines added (green) and removed (red) by edits during the current session. A running cumulative total, not per-message.

Source: statusline.py, segment_lines

def segment_lines(ctx: dict) -> str:
    cost = ctx.get("cost") or {}
    added = cost.get("total_lines_added") or 0
    removed = cost.get("total_lines_removed") or 0
    return f"{GREEN}+{added}{RESET}/{RED}-{removed}{RESET}"

What it reads from the payload: ctx.cost.total_lines_added and ctx.cost.total_lines_removed. Weirdly, these live under cost rather than a dedicated edits section — that's just how Claude Code sends them. Blame the payload shape.

These are the line counts from Edit/Write tool use, accumulated across every turn. If you ran the session's edits through git diff --stat, the numbers would be approximately equal to what this segment shows.

Why two colors on one segment: These are paired values — additions and deletions from the same body of edits. Green/red is conventional diff coloring (git, GitHub, every diff viewer). Using both side-by-side reads as a diff stat at a glance; you don't need to read "plus" and "minus" literally because the colors do that work.

Why the / separator (not a space or middot): The +N/-M format matches git diff --shortstat. Users coming from git already know how to read this layout. A space (+42 -8) would work but wouldn't trigger the same "oh it's like git" recognition; a middot (+42 · -8) would make it feel like two separate facts rather than one diff stat.

Why not a percentage or ratio: Raw counts are what you want here. +156/-23 tells you "modest edit session"; +3552/-464 tells you "heavy session" (that's the screenshot above). A ratio would compress both into the same "7:1" which loses scale. You care about scale in this field.

The or 0 guards: Same pattern as cost and tokens — these fields can be null in early-session payloads. cost.get("total_lines_added") or 0 turns any of {missing, null, zero} into zero, which renders identically as +0. No crash, no blank.

Always renders something (even at zero): +0/-0 is legitimate output — it tells you "no edits yet this session" which is distinct from "I don't know." Same always-visible design as context: a slot that shouldn't disappear just because its value is boring.

Edge cases:

  • Fresh session, no edits → +0/-0.
  • Added but no removes → +42/-0, colors still both present.
  • Large refactor → +3552/-464, no comma formatting (keeps the bar compact).
  • Either field null → treated as zero.
  • cost section absent → both or 0+0/-0.

Typical use: The default preset's edits indicator. Useful for gauging session effort — a three-hour debugging session showing +8/-3 tells a different story than one showing +1247/-623. If you care about which files changed, that's what git status is for; if you just want "how much work got done," this is the glance.

One nitpick I could have fixed but didn't: Big numbers don't get comma-formatted (unlike the raw tokens in context_bar). At +3552 it's fine; at +18473 you'd want +18,473. If you regularly do sessions that edit 10k+ lines, open an issue — that's a one-character fix (f"{added:,}").

18. duration

duration segment showing 6h 38m

python3 statusline.py --set duration
How it works

What it shows: Elapsed session time — formatted as 1h 23m, 45m, or 12s in green. Wall-clock time since the session started, not just time Claude was actively thinking.

Source: statusline.py, segment_duration

def segment_duration(ctx: dict) -> str:
    ms = (ctx.get("cost") or {}).get("total_duration_ms")
    if not ms:
        return ""
    return f"{GREEN}{fmt_duration(ms)}{RESET}"

What it reads from the payload: ctx.cost.total_duration_ms — milliseconds of wall-clock time since the session started. Claude Code tracks and increments this with each payload.

Why if not ms: return "" (hiding on zero/missing): Unlike lines (which shows +0/-0 as a meaningful "no work yet" state), a duration of 0 or null just means Claude Code hasn't sent the field yet — which happens on the very first render. Rendering 0s in that case would be visual noise because it's always 0s until the first event completes. Hiding the segment until there's data is cleaner than flashing a momentary zero.

How fmt_duration formats the milliseconds:

def fmt_duration(ms: int | float | None) -> str:
    if not ms or ms < 0:
        return "0s"
    total_s = int(ms) // 1000
    hours, rem = divmod(total_s, 3600)
    minutes, seconds = divmod(rem, 60)
    if hours:
        return f"{hours}h {minutes}m"
    if minutes:
        return f"{minutes}m {seconds}s" if seconds and minutes < 10 else f"{minutes}m"
    return f"{seconds}s"

Three display tiers, tuned for readability:

  • Under a minute → raw seconds: 42s.
  • Under an hour → minutes, with seconds shown only in the first 10 minutes: 3m 12s up to 9m 59s, then 10m, 11m, 12m (no seconds). Once you're past ten minutes, specific-second precision is noise — you don't say "ten minutes and thirty-seven seconds," you say "about ten minutes."
  • Over an hour → hours and minutes: 6h 38m (the screenshot) or 3h 0m (minutes always shown, even if zero, for consistency).

This means the segment width stays stable as time passes — 4m 12s (7 chars), then 10m (3 chars), then 6h 38m (6 chars). The "drop seconds after 10 minutes" rule is what gives you that stability. Without it, you'd see 10m 0s, 10m 1s, 10m 2s with the segment width wiggling every tick.

Why the ms < 0 guard: fmt_duration is defensive against negative values from clock skew or payload corruption. -5 milliseconds becomes 0s rather than a weird negative render.

Why green: Session time is in the "positive accumulating thing" register — same palette as cost and lines added. No warning color for duration because no wall-clock time is "bad"; you either want to know how long it's been or you don't.

Edge cases:

  • Fresh session, before first message → total_duration_ms absent or zero → segment empty, skipped.
  • First second → 1s.
  • One minute exact → 1m (no seconds since they're zero).
  • One minute, 30 seconds → 1m 30s.
  • Exactly ten minutes → 10m (no seconds shown).
  • One hour, zero minutes → 1h 0m (minutes always shown).

Typical use: Session-tracking for long-running work. Part of the session_timer preset. If you want to notice when a debug session has quietly stretched into "it's been six and a half hours" territory (like the screenshot), this is the gentle reminder.

19. api_time

api_time segment showing api 1h 10m

python3 statusline.py --set api_time
How it works

What it shows: api <duration> in dim gray. The time Claude spent thinking — cumulative wall-clock time your session has been blocked waiting on API responses. A subset of duration.

Source: statusline.py, segment_api_time

def segment_api_time(ctx: dict) -> str:
    ms = (ctx.get("cost") or {}).get("total_api_duration_ms")
    if not ms:
        return ""
    return f"{DIM}api {fmt_duration(ms)}{RESET}"

What it reads from the payload: ctx.cost.total_api_duration_ms — milliseconds accumulated across every API call. Claude Code increments this whenever a response completes.

How it's different from duration:

duration api_time
measures wall clock from session start cumulative API response time
includes your reading/typing yes no
format 1h 23m api 1h 10m
prefix label none api
segment color green dim gray

If your session has been running for six and a half hours (like the duration screenshot above) and this segment reads api 1h 10m, that means Claude was actively generating for ~70 minutes of those 6h 38m — the remaining ~5.5 hours were you reading, typing, thinking, getting coffee. In a well-paced session, api_time is typically 10–30% of duration.

Why the api prefix label: Without it, 1h 10m next to 6h 38m would be ambiguous — two durations, which is which? api 1h 10m makes the role explicit. The prefix is part of the segment, not a separator, so it stays glued to the number when the dispatcher joins segments.

Why dim: api_time is secondary data. duration tells you "how long has this session been"; api_time tells you "of that, how much was spent waiting on the model." Dim signals the latter as supporting information — your eye focuses on duration (bright green) and pulls in api_time when you want the detail. Same visual-hierarchy pattern as cost_tokens and context_bar.

Why the same fmt_duration formatter: Because this IS a duration, just measured differently. Reusing the formatter means the tier rules (seconds-only under 10 minutes, hours+minutes above an hour) behave identically to duration, and the segment widths change predictably.

Same if not ms: return "" guard: Before the first API call finishes, total_api_duration_ms is absent or zero. Rendering api 0s during that window is noise; hiding until there's real data is cleaner. Once the session makes any API call, the segment appears and stays.

Why this matters for session analysis: The api_time / duration ratio tells you a lot about your pacing:

  • Low ratio (~5%) → you're thinking more than Claude. Deep review or careful reading.
  • Medium (~20–30%) → healthy back-and-forth. Typical collaborative session.
  • High (~60%+) → you're mostly watching Claude work. Probably agent-heavy or auto-pilot mode.

None of those are "bad," but the ratio is a useful gut check. The screenshots above together give ~18% (1h 10m of 6h 38m) — a session where the human is doing most of the thinking.

Edge cases:

  • Fresh session, no API calls yet → segment empty, skipped.
  • Single quick call → api 1s.
  • Long thinking task → api 12m 34s.
  • Session spans hours with periodic bursts → api 42m.

Typical use: Part of the session_timer preset. Rarely interesting on its own; usually paired with duration so you can see both the wall clock and the API subset side by side.

20. clock

clock segment showing 4:27pm Sat

python3 statusline.py --set clock
How it works

What it shows: Current wall time in green, weekday abbreviation in dim gray — e.g. 4:27pm Sat.

Gotcha: clock is both a segment and a preset. Use --set clock for the segment alone; use --preset clock to apply the three-segment preset (model, cwd, clock).

Source: statusline.py, segment_clock

def segment_clock(_ctx: dict) -> str:
    # Ticks only when Claude Code sends a render event unless the statusLine
    # block in settings.json sets "refreshInterval" (milliseconds).
    now = datetime.now().astimezone()
    hour = now.strftime("%-I").lower()
    ampm = now.strftime("%p").lower()
    minutes = now.minute
    time_str = f"{hour}{ampm}" if minutes == 0 else f"{hour}:{minutes:02d}{ampm}"
    return f"{GREEN}{time_str}{RESET} {DIM}{now.strftime('%a')}{RESET}"

What it reads from the payload: Nothing. Note the _ctx underscore prefix — the argument is accepted for interface compatibility (every segment takes ctx) but isn't used. Clock pulls its data from datetime.now(), not from Claude Code.

This is the only segment that doesn't read from ctx. Every other segment derives its output from something Claude Code sent in the payload. The clock is purely local: Claude Code doesn't know what time it is where you are, so the status-line script handles it itself.

The ticking problem — why this segment needs special configuration:

Most segments re-render when Claude Code sends a payload, which happens on natural events (assistant message, permission change, vim toggle). That's fine for cost, tokens, rate_limits — they only change when the session does something.

Clock changes continuously. Without a render event, the clock segment freezes on whatever time it was when the last event fired. If Claude is idle for an hour, your "clock" still shows the time from an hour ago — obviously wrong.

The fix is in ~/.claude/settings.json:

{
  "statusLine": {
    "type": "command",
    "command": "python3 /absolute/path/to/usage/statusline.py",
    "padding": 1,
    "refreshInterval": 30000
  }
}

refreshInterval is in milliseconds. 30000 = 30-second refreshes. Claude Code re-invokes the status-line command on that interval in addition to normal events, so the clock keeps current.

Why datetime.now().astimezone() (not datetime.now() alone): .astimezone() attaches the system's local timezone. Without it, datetime.now() produces a naïve datetime that strftime formats using whatever assumptions the C library has. .astimezone() guarantees the output reflects the local wall clock.

Time formatting — same pattern as fmt_reset:

  • %-I → hour in 12-hour form without leading zero (2 not 02).
  • %p → AM/PM, lowercased.
  • Minute conditional: 2pm on the hour, 2:37pm otherwise. Saves three characters when on the hour, which is often enough to matter.
  • %a → weekday abbreviation (Sat).

Why green for the time, dim for the weekday: Hierarchy. The time is the primary signal — glance at the bar, see what time it is. The weekday is context for when you want it (is it actually Saturday afternoon?) but not the primary read.

Why not show the date: Space. Adding Nov 18 or 11/18 to 4:27pm Sat doubles the segment width. Most sessions care about the time plus "is it still today?"; the weekday answers that. If you've been coding for three days straight and want the date, add a separator segment or write your own.

Edge cases:

  • System clock wrong → segment shows the wrong time. The script trusts datetime.now().
  • Daylight-saving transitions → handled by .astimezone(), no special-case needed.
  • No refreshInterval set → clock only updates on Claude Code events, appears stale during idle periods. Most common "why isn't my clock ticking" issue.

Typical use: The clock preset combines this with model and cwd for a time-aware minimal bar. Useful for sessions where you want to notice "wait, it's already 4pm" without checking another clock.

21. worktree

worktree segment simulated with feature-plugins

python3 statusline.py --set worktree

Simulated render — Claude Code only populates ctx.worktree when launched from a git worktree add directory, so the live bar is empty in a normal clone. The image was generated by piping a fake payload through the script: echo '{"worktree":{"name":"feature-plugins"}}' | python3 statusline.py. The glyph is (U+2387) rendered in Fira Code Nerd Font.

How it works

What it shows (when active): ⎇ <worktree-name> in orange. Empty when ctx.worktree is absent.

Gotcha: worktree is both a segment and a preset. Use --set worktree for the segment alone; use --preset worktree to apply the three-segment preset (model, cwd_branch, worktree).

Source: statusline.py, segment_worktree

def segment_worktree(ctx: dict) -> str:
    wt = ctx.get("worktree") or {}
    name = wt.get("name") or wt.get("path") or ""
    if not name:
        return ""
    return f"{ORANGE}{name}{RESET}"

What it reads from the payload: ctx.worktree.name first, then ctx.worktree.path as fallback. Claude Code includes this block only when you launched it from a worktree directory (a tree created by git worktree add).

The name/path fallback matters: Different Claude Code versions populate worktree differently — some send {"name": "feature-x"}, others just {"path": "/path/to/worktree"}. The segment tries name first (preferred, cleaner label), falls back to path (still informative), gives up if both are absent.

Why orange: Worktrees are an "unusual state" signal — you're not on your main checkout, you're on a parallel one. Orange is the palette's "heads up" color. Not a warning (worktrees are fine), but worth noticing so you don't commit to the wrong place.

Why the symbol (U+2387): It's the standard "branch/merge" glyph in Unicode, used by some prompt tools (powerlevel10k, starship) and git GUIs. Nerd Fonts render a stylized version; other fonts render a three-pronged fork shape.

The "empty when not in a worktree" design is a feature: This segment is meant to appear specifically when you're in a worktree. If you always want to see a "worktree: no" badge when not in one, that's a different segment (not currently in the repo — easy to add).

Edge cases:

  • Launched from a normal clone → segment empty, skipped.
  • ctx.worktree exists but both name and path are null → segment empty.
  • Only path set, not name → falls back to path string.
  • Claude Code version doesn't send worktree at all → segment empty.

Typical use: The worktree preset combines this segment with model and cwd_branch for a worktree-aware bar. Useful if you routinely have multiple worktrees of the same repo checked out at different branches.

22. exceeds_200k

exceeds_200k segment showing [200k+] badge

python3 statusline.py --set exceeds_200k
How it works

What it shows: A bright-red [200k+] badge when ctx.exceeds_200k_tokens is true. Empty otherwise.

Source: statusline.py, segment_exceeds_200k

def segment_exceeds_200k(ctx: dict) -> str:
    return f"{BRIGHT_RED}[200k+]{RESET}" if ctx.get("exceeds_200k_tokens") else ""

Four lines total — the simplest segment in the whole file. Fitting as the finale.

What it reads from the payload: ctx.exceeds_200k_tokens, a single boolean. No nested dict, no fallbacks, no formatting logic.

Why this segment exists: Claude API has a pricing tier boundary at 200k input tokens — requests that cross that line cost meaningfully more per token. For 1M-context Opus you can keep working past 200k, but the money meter ticks faster. This segment is a visual nudge: heads up, each additional input token past this point is pricier.

Most sessions on standard Sonnet (200k max) never see this because they compact before hitting it. On 1M-context Opus it's easy to cross without noticing — hence the dedicated segment.

Why BRIGHT_RED (not regular RED): The palette uses RED for normal warnings — over-80% bars, -lines removed, bad-JSON errors. Those are all "something is slightly off" signals. BRIGHT_RED is reserved for "pay attention, this is an unusual state" — this badge and the statusline: bad JSON crash message are the only two places it appears. Using bright red here makes the badge pop even in a busy bar.

Why square brackets around the text: The [...] wrapping reads as a "badge" visually — a labeled sticker rather than flowing text. Same convention GitHub and CI tools use ([DEPRECATED], [FAILED]). The bracket framing signals "this is a status flag, not a sentence."

Why empty when under 200k: Same design as worktree — this segment is meant to appear when the condition triggers. Having a [200k-] badge or "200k: no" would be noise 99% of the time.

Why ctx.get("exceeds_200k_tokens") (no fallback chain): Simplest pattern in the file. .get() returns None if the key is missing, and None is falsy, so the ternary hits the empty branch. If the value is False, same path. Only True renders the badge. No (ctx.get("x") or {}).get("y") dance because exceeds_200k_tokens is a top-level boolean.

Edge cases:

  • Under 200k → empty, skipped.
  • At exactly 200k → depends on Claude Code's threshold; segment just honors the boolean.
  • Field missing from payload → ctx.get(...) returns None → empty.
  • exceeds_200k_tokens: null → same as missing → empty.

Typical use: The context_bar preset combines this segment with model and context_bar so you see a badge pop up the moment you cross into higher-cost territory. Otherwise it's tucked into custom bars as a pricing alert for long sessions.

23. context_bar_short

context_bar_short segment

python3 statusline.py --set context_bar_short
How it works

What it shows: A compact context view — an 8-cell progress bar (matching rate_limits_bars width), the percentage, and a ←in/out→ max triple using k/M notation instead of raw numbers. About half the width of context_bar.

Source: statusline.py, segment_context_bar_short

def segment_context_bar_short(ctx: dict) -> str:
    cw = ctx.get("context_window") or {}
    used = cw.get("used_percentage")
    total_in = cw.get("total_input_tokens") or 0
    total_out = cw.get("total_output_tokens") or 0
    window = cw.get("context_window_size") or 0
    used_str = f"{used}%" if used is not None else "-"
    # Context-window sizes are always round (200k, 1M) — render without trailing .00.
    if window >= 1_000_000 and window % 1_000_000 == 0:
        window_str = f"{window // 1_000_000}M"
    elif window >= 1000 and window % 1000 == 0:
        window_str = f"{window // 1000}k"
    else:
        window_str = human_tokens(window) if window else "?"
    return (
        f"{bar(used, 8)} "
        f"{pct_color(used)}{used_str}{RESET} "
        f"{DIM}{human_tokens(total_in)}/{human_tokens(total_out)}→"
        f" {window_str} max{RESET}"
    )

What's different from context_bar:

context_bar context_bar_short
bar width 20 cells 8 cells
in/out/max format raw with commas (15,234 in / 4,521 out / 200,000 max) k/M (←15.2k/4.5k→ 200k max)
arrows none (text labels) ←/→ brackets around in/out pair
total width ~64+ chars ~36 chars

The screenshot above is from a real session: 47% ←1.9k/312.3k→ 1M max. Input is tiny (1.9k) because most of the context is the session's own accumulated output (312.3k) over hours of walkthrough writing — one of those weird cases where output dwarfs input.

Why an 8-cell bar (not 20): The whole point of this segment is to fit alongside other segments in a busy bar. At 8 cells each cell represents 12.5% — coarser than the 20-cell version's 5% per cell, but enough resolution to see "empty / quarter / half / near-full" at a glance. Matching rate_limits_bars width keeps the bar heights aligned visually when both segments are active.

Why the arrow brackets (←X/Y→) instead of labels: Space. 15.2k in / 4.5k out is 19 chars of which in and out are just labels you read once and then ignore. The arrows compress to 3 chars of notation (, , /) and your eye accepts "first number is in, second is out" from position. Left-pointing arrow at the start visually holds the pair together as a unit.

Why the max-formatting special-case: human_tokens(1_000_000) returns 1.00M because the formatter uses .2f for M-scale (useful when the value is 1.23M). For context-window sizes, which are always round (200_000, 1_000_000), 1.00M max looks pedantic. The inline check strips trailing zeros when the value is an exact multiple — 1M max, 200k max, 1.5M max (if Anthropic ever ships a 1.5M window).

Edge cases:

  • used_percentage is null → 8 dim dots, - for percentage, tokens still render.
  • Fresh session → ········· - ←0/0→ 200k max (dotted bar, dash percentage, zeros for counts).
  • All token fields null → each or 0←0/0→ ? max if window is also null.
  • Window is not a round multiple → falls back to human_tokens (1.23M max).

Typical use: Drop-in replacement for context_bar when you want context monitoring but the wider version would push your bar past the terminal edge. Pairs well with rate_limits_bars since both use 8-cell bars at the same scale.

24. context_bar_long

context_bar_long segment

python3 statusline.py --set context_bar_long
How it works

What it shows: The full context view — 8-cell bar, percentage, and the raw (N in / N out / N max) token counts in dim gray. This is what context_bar used to render before it was split into a bar-only variant.

Source: statusline.py, segment_context_bar_long

def segment_context_bar_long(ctx: dict) -> str:
    cw = ctx.get("context_window") or {}
    used = cw.get("used_percentage")
    total_in = cw.get("total_input_tokens") or 0
    total_out = cw.get("total_output_tokens") or 0
    window = cw.get("context_window_size") or 0
    used_str = f"{used}%" if used is not None else "-"
    return (
        f"{bar(used, 8)} "
        f"{pct_color(used)}{used_str}{RESET} "
        f"{DIM}({total_in:,} in / {total_out:,} out / {window:,} max){RESET}"
    )

What it reads from the payload: Four fields under ctx.context_window:

  • used_percentage
  • total_input_tokens
  • total_output_tokens
  • context_window_size — varies by model (1M for 1M-context Opus, 200k for standard Sonnet).

Why {:,} comma formatting: Raw token counts get big fast. 263325 is hard to parse at a glance; 263,325 reads as "two hundred sixty-three thousand" immediately. Three extra characters for a clear readability win.

Why dim for the raw counts, bright for the bar+percentage: Same visual hierarchy as cost_tokens. The bar+percentage is the primary signal (am I near the limit?); the token counts are secondary (how much in absolute terms?). Dim on the counts lets your eye land on the bar first, then pull in the numbers when you want them.

Why max is always shown even when it's a round number: Consistency with the comma-formatted in/out values. Using 1,000,000 max next to 1,745 in / 263,325 out keeps the triple visually uniform. context_bar_short takes the opposite trade-off and uses 1M max specifically because its whole point is compact rendering.

Edge cases:

  • used_percentage is null → 20 dim dots, - for percentage, raw counts still show with whatever values they have (often zero).
  • Fresh session → ·····...· - (0 in / 0 out / 200,000 max). Still informative: you see the window size.
  • All three token fields null → each or 0 → zeros. No crash.
  • context_window_size is null0 max in the suffix. Reads weird but won't crash.

Typical use: The context_bar preset expands to include this segment. For sessions where you want both the visual bar and the exact numbers — deep debugging, careful scope management, or any time you want to verify the percentage against real counts.

Development

Lint and format:

ruff check . && ruff format .

Run the test suite (every segment and every preset × {sample, empty, nulls-everywhere}, helpers, dispatcher, migration):

python3 -m pytest

Watch the render log while Claude Code is running:

tail -f statusline.log

Smoke-test a specific payload shape:

echo '{"cwd":"/tmp","model":{"display_name":"Opus"}}' | python3 statusline.py
echo '{}' | python3 statusline.py

Log rotation

statusline.log rotates at 10 MB with three backups (.1 / .2 / .3). Total disk ceiling is roughly 40 MB. DEBUG-level logging stays on so crashes are fully diagnosable.

License

MIT. See LICENSE.

About

Composable status line for Claude Code. 24 segments, 9 presets, one Python file.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages