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.
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.
python3 statusline.py --set cwd_branch_dirty,tokens,rate_limits_bars,context_bar,lines,duration,exceeds_200kEight 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.
- Python 3.11 or newer
- No runtime dependencies (standard library only)
giton$PATHif you use thedirty/cwd_branch_dirtysegments
Clone somewhere permanent. The path gets baked into your Claude Code settings.
git clone <repo-url> ~/code/usageThen 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.
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. |
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 |
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 payloadSeveral 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):
STATUSLINE_PLUGINenvironment variable (treated as a preset name)./active_segmentsfile- Built-in default preset
One-shot override that doesn't touch the saved selection:
STATUSLINE_PLUGIN=context_bar claudeMost 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
}
}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- 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_branchreads.git/HEADdirectly).git_dirtyis the single exception: onegit statuscall with a 150 ms timeout, fails closed. - Defend against missing fields. Optional sections like
rate_limits,vim,agent,worktree, andsession_nameare absent, not null. Numeric fields likecontext_window.used_percentagecan be null early in a session. Use(ctx.get("x") or {}).get("y") or defaultrather than bracket access.
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. |
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.
python3 statusline.py --set modelHow 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": nullin the payload →?.display_nameis 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.
python3 statusline.py --set cwdHow 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:
safe_cwd()— collapses$HOMEto~(so/home/you/code/foobecomes~/code/foo), and falls back to?ifctx.cwdis missing or empty.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/…/LambdaNodestill 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.cwdabsent,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.
python3 statusline.py --set cwd_shortHow 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.cwdabsent →?.- At filesystem root
/→/rendered. - At home directory
$HOME→~rendered. - Trailing-slash paths like
/foo/bar/→bar(Python'sPath.namestrips 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.
python3 statusline.py --set cwd_branchHow 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 outWhat 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:
HEADcontainsref: refs/heads/<name>→ returns<name>. Normal on-a-branch case.HEADcontains a raw commit SHA (detached HEAD) → returns the first 7 characters.- No
.gitfound walking up, or unparseableHEAD→ returnsNone.
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 addkind) → walks the pointer file in.gitto 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.
python3 statusline.py --set cwd_branch_dirtyHow 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 statustimed out, orgitnot 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 outWhat 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
gitnot on PATH → branch shown dim (unknown). - In a repo but
git statustakes >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.
On main:
On a feature branch with a slash in the name:
python3 statusline.py --set branchHow 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,costRenders 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/HEADunreadable → 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.
python3 statusline.py --set dirtyHow 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,costThat 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,dirtyMost 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.
gitunavailable /git statustimeout → empty (fail closed, same ascwd_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.
python3 statusline.py --set costHow 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:
ctx.costmissing → falls back to{},.get("total_cost_usd")returnsNone,or 0.0kicks in →$0.0000.ctx.costpresent buttotal_cost_usdmissing →.get(...)returnsNone,or 0.0→$0.0000.ctx.cost.total_cost_usdisnull→.get(...)returnsNone,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.9318pictured) → renders the same way, no truncation. total_cost_usdnull 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.
python3 statusline.py --set tokensHow 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_window — total_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.8kor227.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.
python3 statusline.py --set cost_tokensHow 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,tokensYou 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.
python3 statusline.py --set rate_5hHow 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:
- Absent section →
ctx.rate_limitssimply 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 isNone. - Present but empty →
rate_limits.five_hourcan beNoneearly 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 REDThree 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_percentageis0→ renders5h 0%in green. Zero is data, not "no data."100%→ renders5h 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.
python3 statusline.py --set rate_7dHow 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 outWhat it reads from the payload: Two fields under ctx.rate_limits.seven_day:
used_percentage— same shape and color bands as the 5-hour version.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-timedatetime, 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.%Igives02;%-Igives2. Makes2pmread naturally instead of02pm.- Minute conditional —
houralone when on the hour (2pm),hour:MMotherwise (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 Noneor0→ returns"", the(... reset)suffix is omitted entirely.fromtimestampraises 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_percentagepresent butresets_atmissing → 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.
python3 statusline.py --set rate_limitsHow 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_limitsis also a preset name. Use--set rate_limitsfor the segment alone; use--preset rate_limitsto apply the two-segment preset (rate_limits_bars, cost). Same disambiguation applies tocontext_bar,clock, andworktree.
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_percentagepresent butresets_atmissing →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.
python3 statusline.py --set rate_limits_barsHow 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)) + RESETStandard 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.
pctis0→ zero filled cells, all dim gray; bar frame still visible.pctis100→ all eight cells filled, red.pctis a float like42.37→ rounded to 3 filled cells (round(8 * 0.4237)= 3), displayed as42%.
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.
python3 statusline.py --set contextHow 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:
- Early in a session,
used_percentageis oftennull— the session hasn't sent enough data to meter. But you still want to seectx -in the bar so you know it's being tracked; the absence of the number is itself data. - The segment always says "ctx", so you always know what's in that slot — never a mystery empty space.
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 = 0→ctx 0%in greenused_percentage = 41→ctx 41%in greenused_percentage = 65→ctx 65%in orangeused_percentage = 92→ctx 92%in redused_percentage = None→ctx -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:
contextis for a quick-glance number, not a visualization. For the bar, usecontext_bar.- 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 atctx 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_percentageoftennull→ctx -in gray. used_percentage: 0→ctx 0%in green (distinguishable from null).- After compaction →
used_percentageresets lower. context_windowsection 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.
python3 statusline.py --set context_barHow 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_baris both a segment and a preset. Use--set context_barfor the segment alone (just bar + percentage). Use--preset context_barto apply the preset, which expands tomodel, 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_percentageisnull→ bar renders as 20 dim dots (····················),-for the percentage.- Fresh session → same dotted bar until Claude Code reports usage.
context_windowsection missing entirely → same as null, segment still renders (the "always visible" design shared withcontext).
Typical use: Minimalist bars where you want context awareness but don't want the long in/out/max triple crowding things.
python3 statusline.py --set linesHow 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. costsection absent → bothor 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:,}").
python3 statusline.py --set durationHow 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 12sup to9m 59s, then10m,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) or3h 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_msabsent 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.
python3 statusline.py --set api_timeHow 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.
python3 statusline.py --set clockHow it works
What it shows: Current wall time in green, weekday abbreviation in dim gray — e.g. 4:27pm Sat.
Gotcha:
clockis both a segment and a preset. Use--set clockfor the segment alone; use--preset clockto 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 (2not02).%p→ AM/PM, lowercased.- Minute conditional:
2pmon the hour,2:37pmotherwise. 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
refreshIntervalset → 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.
python3 statusline.py --set worktreeSimulated 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:
worktreeis both a segment and a preset. Use--set worktreefor the segment alone; use--preset worktreeto 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.worktreeexists but bothnameandpathare null → segment empty.- Only
pathset, notname→ falls back to path string. - Claude Code version doesn't send
worktreeat 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.
python3 statusline.py --set exceeds_200kHow 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(...)returnsNone→ 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.
python3 statusline.py --set context_bar_shortHow 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_percentageisnull→ 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→ ? maxif 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.
python3 statusline.py --set context_bar_longHow 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_percentagetotal_input_tokenstotal_output_tokenscontext_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_percentageisnull→ 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→ eachor 0→ zeros. No crash. context_window_sizeisnull→0 maxin 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.
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 pytestWatch the render log while Claude Code is running:
tail -f statusline.logSmoke-test a specific payload shape:
echo '{"cwd":"/tmp","model":{"display_name":"Opus"}}' | python3 statusline.py
echo '{}' | python3 statusline.pystatusline.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.
MIT. See LICENSE.























![exceeds_200k segment showing [200k+] badge](/ludothegreat/usage/raw/main/screenshots/22-exceeds_200k.png)

