Customize Your Claude Code Status Line to Manage Token Burn

26 min read

Customize the Claude Code status line to track context window usage, session limits, and weekly quotas so you stop burning tokens you don't need.


Tokens are the new currency of software development, and many developers just got a very expensive lesson in exchange rates.

I’m talking about those of you developers using GitHub Copilot.

In late April 2026, GitHub announced that Copilot was moving to usage-based billing, replacing the premium request model with a pool of AI credits (AICs) metered by actual token consumption. The change took effect on June 1, 2026, and the reaction from developers has been… not great. TechCrunch covered the backlash, with developers reporting projected costs jumping from around $29 per month to nearly $750 for the same workloads. Power users running agentic sessions are estimating increases of 10x to 50x, and the fallback to lower-cost models when you exhaust your allotment? Gone.

The community discussion thread tells you everything you need to know about how this landed… and the /r/GitHubCopilot subreddit is on fire!

The result is a wave of developers re-evaluating their agentic coding tools. Some are staying and adapting, but plenty are moving to alternatives. I prefer Claude Code over all of them — it’s the tool I’ve built my agentic engineering workflows around and the one I write about on Voitanos — but you may land somewhere else.

Here’s the thing though: no matter which tool you use, the era of “don’t think about tokens, just prompt” is over. Token consumption now directly affects what you pay or how quickly you hit your plan’s limits. The developers who understand and manage their consumption get dramatically more out of the same subscription than those who don’t.

That’s why the single most valuable customization I’ve made to Claude Code is my status line. It puts my token and context window consumption in front of me after every single turn, so managing it becomes a habit instead of a surprise.

The Claude Code status line is a configurable terminal display that runs a script of your choosing after every turn. A custom script gives you real-time visibility into the three numbers that determine how far your subscription goes: how full your context window is, how much of your 5-hour session limit remains, and where you stand on your 7-day rolling quota.

Why the status line is worth customizing

Claude Code lets you replace its default status line with the output of any script you want. After every turn, it runs your script and hands it a JSON payload describing the current session: the model you’re using, how full your context window is, and how much of your rate limits you’ve consumed. Whatever your script prints is what you see at the bottom of your terminal.

Most status line examples you’ll find focus on cosmetics, like showing your git branch or some color flair. That stuff is nice, but it buries the lede. The real value is visibility into your context window and usage quotas, because that’s the data that changes how you work.

Let me explain why that matters.

Every prompt you submit to a large language model (LLM) re-sends the entire conversation history to the model. That’s how LLMs work; they’re stateless, so the full context goes up with every request. When your session has accumulated a long history of file reads, tool calls, and back-and-forth, you’re paying to resubmit all of it on every single turn, even when most of that history adds zero value to what you’re asking right now.

You’re essentially burning tokens for nothing. And those tokens count against your 5-hour session window and your 7-day rolling quota. Burn them carelessly and you’ll hit your limits hours or days before you should.

What the Claude Code status line displays

Here’s what mine looks like:

A three-line Claude Code status line showing the current working folder, the logged-in account and model with context window size, and color-coded usage meters for the context window, 5-hour session limit, and 7-day rolling quota

My customized Claude Code status line

It renders three lines, each answering a different question.

Line 1: where am I?

A blue terminal line showing the abbreviated current working directory path and, when in a git repository, the active branch name.

Line 1: Get your bearings...

The first line shows the current folder I’m working in. Simple, but when you’re running multiple Claude Code sessions across different projects, it’s the fastest way to confirm you’re about to prompt the right one. When I’m in a git repository, this line also shows the current branch.

Line 2: who am I, and what am I driving?

A terminal line showing the logged-in Claude account email address, the active model ID (claude-opus-4-8[1m]), and total context window size (1,000k tokens).

Line 2: Current account, model, and context window size

The second line has two segments:

  • Account: the email address of the Claude account I’m logged into. I have both work and personal Claude subscriptions, and this tells me at a glance which one this session is billing against. If you’ve ever accidentally burned your personal quota on work tasks, you know why this segment exists.
  • Model and context window size: the model ID for the current session and the total size of its context window. In the screenshot, that’s claude-opus-4-8[1m] with a 1,000k token context window.

Line 3: how much runway do I have?

A terminal line showing a green gradient progress bar at 7% context usage, the 5-hour session limit at 1% with reset time, and the 7-day quota at 4% with reset time — all green indicating low consumption.

Line 3: Size of last turn's context window, session & weekly status

This is the line that earns its keep. Three segments, all about consumption:

  • Context window usage: a progress bar and a percentage showing how full my context window is as of the last turn. In the screenshot I’m at 7% with a green light, so I’m in a good state. The bar fills as the conversation grows, the percentage changes color from green to yellow to red as I approach the limit, and the token count shows the raw total going up with the next prompt I submit.
  • Session limit: my 5-hour session window. The screenshot shows I’ve used 1% of it, and it resets in 4 minutes and 54 seconds.
  • Weekly limit: my 7-day rolling quota. I’ve used 4%, and it resets in 4 days and 4 hours.

One glance and I know exactly where I stand on every meter that matters.

How to use context window data to manage token consumption

The context window segment drives a simple habit: when the context grows large and the history isn’t earning its keep, I compact or clear the session.

There’s no magic number… it all depends on the scenario. But when it starts getting big and I’m using a more powerful model that uses more tokens, I ask myself whether the next thing I’m doing needs all that accumulated history. If it doesn’t, I run /compact to summarize the session down to its essentials, or /clear to start fresh. In practice, compacting at 60% rather than waiting for Claude Code to force-compact typically cuts the tokens I’m resubmitting by roughly half — which directly stretches how far my session and weekly quotas go.

The choice between them is simple: use /compact when you want the model to carry forward a summary of what you’ve done; use /clear when you’re switching to a completely different task and the accumulated context is just noise.

Without the status line, context growth is invisible until Claude Code force-compacts on its own schedule or you slam into a limit mid-task. With it, the green-yellow-red progression gives me plenty of warning to compact at a natural breakpoint I choose, like right after finishing a feature, instead of in the middle of something important.

The session and weekly segments shape my planning the same way. If I can see my 5-hour window is nearly exhausted but resets in twenty minutes, I’ll grab a coffee or churn through some email instead of starting a big refactor. If my weekly quota is running hot on a Tuesday, I know to be more deliberate about which tasks I hand to the agent for the rest of the week. That’s the same discipline I cover in Replicate Your Hands, Not Your Brain: be intentional about what you delegate to an agent, because every delegation has a cost.

Going over the limit

Here’s what makes Claude Code’s pricing model more forgiving than it first appears: when you exhaust your session or weekly limit, it automatically switches to metered pricing and pulls from any pre-purchased credits you have on hand. You keep working; the billing just shifts to your credit balance. Credits are available at a discount off regular metered rates — the exact discount depends on your Claude Max subscription tier. If you have auto-reload enabled, Claude tops up your credits automatically when they run low, so work continues uninterrupted unless your balance hits zero with auto-reload off.

The Claude Code credits purchase dialog showing available credit bundles with discounted pricing.

Purchase additional credits to keep working

Usage credit pricing & discounts

Discounts vary depending on your Claude Max subscription. The discounts above are for the $100/mo Max x5 plan.

This is great when you can’t afford to pause your work!

How the Claude Code status line script works

I maintain two versions of the script so it covers every platform:

  • statusline.sh: a bash script for anyone running Claude Code from a macOS or Linux terminal, or on Windows inside WSL or Git Bash. This is the one I use on macOS.

    click to expand and see the statusline.sh for macOS, Linux, and Windows (WSL / Git Bash) consoles
      1#!/usr/bin/env bash
      2# ~/.claude/statusline.sh
      3# Claude Code statusLine script
      4# Toggle: set CLAUDE_STATUSLINE_BAR=0 to hide the context bar (keep just %)
      5# Line 1: <cwd> | [branch]
      6# Line 2: [email |] <model-id> [ctx-size]
      7# Line 3: <ctxbar> <ctx%> [tok] | [s:<5h%> (<rel>)] | [w:<7d%> (<rel>)]
      8
      9# ---------------------------------------------------------------------------
     10# ANSI colors
     11# ---------------------------------------------------------------------------
     12RESET='\033[0m'
     13GREEN='\033[32m'
     14YELLOW='\033[33m'
     15BLUE='\033[34m'
     16RED='\033[31m'
     17DIM='\033[2m'
     18
     19SEP="${DIM} | ${RESET}"
     20
     21# Truecolor helper (24-bit). Falls back gracefully on terminals that ignore it.
     22rgb() { printf '\033[38;2;%d;%d;%dm' "$1" "$2" "$3"; }
     23
     24# Toggle: set CLAUDE_STATUSLINE_BAR=0 to hide the context bar (keep just %)
     25SHOW_BAR="${CLAUDE_STATUSLINE_BAR:-1}"
     26BAR_WIDTH="${CLAUDE_STATUSLINE_BAR_WIDTH:-10}"
     27
     28# ---------------------------------------------------------------------------
     29# Read all fields. Prefer jq; fall back to a grep/sed parser when jq is absent
     30# (e.g. a fresh Windows Git Bash install) so the statusline still renders.
     31# ---------------------------------------------------------------------------
     32input=$(cat)
     33
     34# Use ASCII Unit Separator (0x1F) as the field delimiter. Unlike tab/space,
     35# bash does NOT collapse consecutive non-whitespace IFS chars, so empty fields
     36# (e.g. absent cwd or rate_limits) are preserved and columns never shift.
     37_US=$'\x1f'
     38
     39if command -v jq >/dev/null 2>&1; then
     40_fields=$(printf '%s' "$input" | jq -j '[
     41  (.workspace.current_dir // .cwd // ""),
     42  (.model.id // ""),
     43  (.context_window.used_percentage | if . != null then tostring else "" end),
     44  (.context_window.total_input_tokens | if . != null then tostring else "" end),
     45  (.context_window.total_output_tokens | if . != null then tostring else "" end),
     46  (.context_window.context_window_size | if . != null then tostring else "" end),
     47  (.rate_limits.five_hour.used_percentage | if . != null then tostring else "" end),
     48  (.rate_limits.five_hour.resets_at | if . != null then tostring else "" end),
     49  (.rate_limits.seven_day.used_percentage | if . != null then tostring else "" end),
     50  (.rate_limits.seven_day.resets_at | if . != null then tostring else "" end)
     51] | join("\u001f")' 2>/dev/null)
     52
     53IFS="$_US" read -r cwd model_id ctx_pct in_tok out_tok ctx_size five_pct five_reset seven_pct seven_reset <<EOF
     54$_fields
     55EOF
     56else
     57  # ---- jq-less fallback -----------------------------------------------------
     58  # Pull scalar fields with grep/sed (POSIX tools present on macOS & Git Bash).
     59  # Strings: capture "key":"value" ; Numbers: capture "key":<number>.
     60  # Nested keys are disambiguated by slicing to the relevant object first.
     61  # Flatten newlines first so slicing works on pretty-printed JSON too.
     62  _flat=$(printf '%s' "$input" | tr '\n' ' ')
     63  _jstr() { printf '%s' "$1" | grep -Eo "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -n1 | sed -E "s/.*:[[:space:]]*\"([^\"]*)\"/\1/"; }
     64  _jnum() { printf '%s' "$1" | grep -Eo "\"$2\"[[:space:]]*:[[:space:]]*-?[0-9]+(\.[0-9]+)?" | head -n1 | sed -E "s/.*:[[:space:]]*(-?[0-9.]+)/\1/"; }
     65  # Crude object slicer: drop everything up to and including "key": { (single line in).
     66  _jobj() { printf '%s' "$1" | sed -E "s/.*\"$2\"[[:space:]]*:[[:space:]]*\{//"; }
     67
     68  cwd=$(_jstr "$_flat" "current_dir"); [ -z "$cwd" ] && cwd=$(_jstr "$_flat" "cwd")
     69  model_id=$(_jstr "$_flat" "id")
     70  _cw=$(_jobj "$_flat" "context_window")
     71  ctx_pct=$(_jnum "$_cw" "used_percentage")
     72  in_tok=$(_jnum "$_cw" "total_input_tokens")
     73  out_tok=$(_jnum "$_cw" "total_output_tokens")
     74  ctx_size=$(_jnum "$_cw" "context_window_size")
     75  _5h=$(_jobj "$_flat" "five_hour")
     76  five_pct=$(_jnum "$_5h" "used_percentage")
     77  five_reset=$(_jnum "$_5h" "resets_at")
     78  _7d=$(_jobj "$_flat" "seven_day")
     79  seven_pct=$(_jnum "$_7d" "used_percentage")
     80  seven_reset=$(_jnum "$_7d" "resets_at")
     81
     82  # One-time install hint (cached so it nags at most once per TTL window).
     83  _jq_hint_file="${TMPDIR:-/tmp}/statusline-jq-hint"
     84  if [ ! -f "$_jq_hint_file" ]; then
     85    case "$(uname -s 2>/dev/null)" in
     86      Darwin*)              _jq_install="brew install jq" ;;
     87      MINGW*|MSYS*|CYGWIN*) _jq_install="winget install jqlang.jq   (or: choco install jq / scoop install jq)" ;;
     88      Linux*)               _jq_install="sudo apt install jq   (or your distro's package manager)" ;;
     89      *)                    _jq_install="see https://jqlang.github.io/jq/download/" ;;
     90    esac
     91    printf 'claude statusline: jq not found — limited output. Install: %s\n' "$_jq_install" >&2
     92    : > "$_jq_hint_file" 2>/dev/null
     93  fi
     94fi
     95
     96[ -z "$cwd" ] && cwd="$PWD"
     97
     98# ---------------------------------------------------------------------------
     99# Relative-time helper: seconds → "Xd Yh" / "Xh Ym" / "Xm Ys" / "Xs" / "now"
    100# ---------------------------------------------------------------------------
    101rel_time() {
    102  local epoch="$1"
    103  local now
    104  now=$(date +%s)
    105  local diff=$(( epoch - now ))
    106  if [ "$diff" -le 0 ]; then
    107    printf 'now'
    108    return
    109  fi
    110  local days=$(( diff / 86400 ))
    111  local hours=$(( (diff % 86400) / 3600 ))
    112  local mins=$(( (diff % 3600) / 60 ))
    113  local secs=$(( diff % 60 ))
    114  if   [ "$days"  -gt 0 ]; then printf '%dd %dh' "$days"  "$hours"
    115  elif [ "$hours" -gt 0 ]; then printf '%dh %dm' "$hours" "$mins"
    116  elif [ "$mins"  -gt 0 ]; then printf '%dm %ds' "$mins"  "$secs"
    117  else                           printf '%ds'     "$secs"
    118  fi
    119}
    120
    121# ---------------------------------------------------------------------------
    122# Token formatter: 15234 → 15.2k, 1500000 → 1.5M
    123# ---------------------------------------------------------------------------
    124fmt_tokens() {
    125  awk -v n="${1:-0}" 'BEGIN {
    126    if (n+0 <= 0)            { printf ""; }
    127    else if (n >= 1000000)   { printf "%.1fM", n/1000000 }
    128    else if (n >= 1000)      { printf "%.1fk", n/1000 }
    129    else                     { printf "%d", n }
    130  }'
    131}
    132
    133# ---------------------------------------------------------------------------
    134# Context-size formatter: always thousands with comma grouping
    135#   1000000 → 1,000k   200000 → 200k   128000 → 128k
    136# ---------------------------------------------------------------------------
    137fmt_ctx_size() {
    138  awk -v n="${1:-0}" 'BEGIN {
    139    if (n+0 <= 0) { printf ""; exit }
    140    k = int((n + 500) / 1000)          # round to nearest thousand
    141    s = sprintf("%d", k)
    142    out = ""
    143    len = length(s)
    144    for (i = 1; i <= len; i++) {
    145      out = out substr(s, i, 1)
    146      rem = len - i
    147      if (rem > 0 && rem % 3 == 0) out = out ","
    148    }
    149    printf "%sk", out
    150  }'
    151}
    152
    153# ---------------------------------------------------------------------------
    154# Segment 1: abbreviated cwd (blue)
    155# ---------------------------------------------------------------------------
    156short_cwd="${cwd/#"$HOME"/~}"
    157seg_cwd="${BLUE}${short_cwd}${RESET}"
    158
    159# ---------------------------------------------------------------------------
    160# Segment 2: git branch (green) — omitted when not in a repo
    161# ---------------------------------------------------------------------------
    162seg_branch=""
    163git_root=$(git -C "$cwd" --no-optional-locks rev-parse --show-toplevel 2>/dev/null)
    164if [ -n "$git_root" ]; then
    165  branch=$(git -C "$cwd" --no-optional-locks symbolic-ref --short HEAD 2>/dev/null)
    166  if [ -z "$branch" ]; then
    167    branch=$(git -C "$cwd" --no-optional-locks rev-parse --short HEAD 2>/dev/null)
    168    branch="(${branch})"
    169  fi
    170  seg_branch="${GREEN}${branch}${RESET}"
    171fi
    172
    173# ---------------------------------------------------------------------------
    174# Segment 3: Claude Code authenticated account email (blue) — fail-silent
    175# Source: `claude auth status` (JSON .email), cached in $TMPDIR for 5 min.
    176# Override: set $CLAUDE_ACCOUNT_EMAIL in the environment to skip the CLI.
    177# Force refresh: rm "${TMPDIR:-/tmp}/statusline-account"
    178# ---------------------------------------------------------------------------
    179account_email=""
    180_cache_file="${TMPDIR:-/tmp}/statusline-account"
    181_cache_ttl=300  # seconds
    182
    183if [ -n "$CLAUDE_ACCOUNT_EMAIL" ]; then
    184  account_email="$CLAUDE_ACCOUNT_EMAIL"
    185else
    186  _need_refresh=1
    187  if [ -f "$_cache_file" ]; then
    188    _mtime=$(stat -f %m "$_cache_file" 2>/dev/null || stat -c %Y "$_cache_file" 2>/dev/null || echo 0)
    189    _cache_age=$(( $(date +%s) - _mtime ))
    190    if [ "$_cache_age" -ge 0 ] && [ "$_cache_age" -lt "$_cache_ttl" ]; then
    191      account_email=$(cat "$_cache_file" 2>/dev/null)
    192      _need_refresh=
    193    fi
    194  fi
    195
    196  if [ -n "$_need_refresh" ] && command -v claude >/dev/null 2>&1; then
    197    _auth_json=$(claude auth status 2>/dev/null)
    198    if [ -n "$_auth_json" ]; then
    199      account_email=$(printf '%s' "$_auth_json" | jq -r '.email // empty' 2>/dev/null)
    200    fi
    201    if [ -z "$account_email" ] || [ "$account_email" = "null" ]; then
    202      account_email=$(claude auth status --text 2>/dev/null \
    203        | grep -Eo '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' \
    204        | head -n1)
    205    fi
    206    [ "$account_email" = "null" ] && account_email=""
    207    printf '%s' "$account_email" > "$_cache_file" 2>/dev/null
    208  fi
    209fi
    210
    211if [ -n "$account_email" ]; then
    212  seg_account="${BLUE}${account_email}${RESET}"
    213else
    214  seg_account=""
    215fi
    216
    217# ---------------------------------------------------------------------------
    218# Segment 4: model id + total context size (size in thousands, e.g. 1,000k)
    219# Segment 5: context bar + context % + session tokens
    220#   bar: RGB gradient green→yellow→red, dynamic emoji, % colored by threshold
    221#   thresholds: <70 green, 70–89 yellow, >=90 red (emoji adds 20% step)
    222# ---------------------------------------------------------------------------
    223build_ctx_bar() {
    224  local pct_int="$1" filled i pos r g b out=""
    225  filled=$(( (pct_int * BAR_WIDTH + 50) / 100 ))
    226  for (( i=0; i<BAR_WIDTH; i++ )); do
    227    if [ "$BAR_WIDTH" -gt 1 ]; then pos=$(( i * 100 / (BAR_WIDTH - 1) )); else pos=0; fi
    228    if [ "$pos" -le 50 ]; then
    229      r=$(( 0 + 220 * pos / 50 )); g=200; b=$(( 80 - 80 * pos / 50 ))
    230    else
    231      local adj=$(( pos - 50 )); r=220; g=$(( 200 - 160 * adj / 50 )); b=$(( 0 + 20 * adj / 50 ))
    232    fi
    233    if [ "$i" -lt "$filled" ]; then
    234      out="${out}$(rgb $r $g $b)█"
    235    else
    236      out="${out}\033[38;2;60;60;60m░"
    237    fi
    238  done
    239  printf '%b' "${out}${RESET}"
    240}
    241
    242# Context usage: percent, color/emoji, and bar — computed first so both
    243# segment 4 (model line) and segment 5 (context line) can reuse them.
    244ctx_int=""
    245bar=""
    246if [ -n "$ctx_pct" ]; then
    247  ctx_int=$(printf '%.0f' "$ctx_pct")
    248  [ "$ctx_int" -lt 0 ] && ctx_int=0
    249  [ "$ctx_int" -gt 100 ] && ctx_int=100
    250
    251  if   [ "$ctx_int" -ge 90 ]; then ctx_color="$RED";
    252  elif [ "$ctx_int" -ge 70 ]; then ctx_color="$YELLOW";
    253  elif [ "$ctx_int" -ge 20 ]; then ctx_color="$GREEN";
    254  else                             ctx_color="$GREEN";
    255  fi
    256
    257  [ "$SHOW_BAR" = "1" ] && bar=$(build_ctx_bar "$ctx_int")
    258fi
    259
    260# Segment 4: model id + context bar + total context size
    261ctx_size_fmt=""
    262if [ -n "$ctx_size" ] && [ "${ctx_size:-0}" -gt 0 ]; then
    263  ctx_size_fmt=$(fmt_ctx_size "$ctx_size")
    264fi
    265if [ -n "$ctx_size_fmt" ]; then
    266  seg_model="${model_id}${SEP}ctx: ${DIM}${ctx_size_fmt}${RESET}"
    267else
    268  seg_model="${model_id}${RESET}"
    269fi
    270
    271# Segment 5: context bar + context % + session tokens
    272if [ -n "$ctx_int" ]; then
    273  if [ -n "$bar" ]; then
    274    seg_ctx="${bar} ${ctx_color}${ctx_int}%${RESET}"
    275  else
    276    seg_ctx="${ctx_color}${ctx_int}%${RESET}"
    277  fi
    278else
    279  seg_ctx="--${RESET}"
    280fi
    281
    282# Session token total (input + output), appended to the context segment when present
    283tok_total=""
    284if [ -n "$in_tok" ] || [ -n "$out_tok" ]; then
    285  sum=$(( ${in_tok:-0} + ${out_tok:-0} ))
    286  tok_fmt=$(fmt_tokens "$sum")
    287  if [ -n "$tok_fmt" ]; then
    288    tok_total="${DIM}(${tok_fmt} tok)${RESET}"
    289    seg_ctx="${seg_ctx} ${tok_total}"
    290  fi
    291fi
    292
    293# ---------------------------------------------------------------------------
    294# Segment 6: 5-hour rate limit — omitted when block absent
    295# ---------------------------------------------------------------------------
    296seg_five=""
    297if [ -n "$five_pct" ] && [ -n "$five_reset" ]; then
    298  five_int=$(printf '%.0f' "$five_pct")
    299  if [ "$five_int" -ge 80 ]; then five_color="$RED"; else five_color="$YELLOW"; fi
    300  five_rel=$(rel_time "$five_reset")
    301  seg_five="s: ${RESET}${five_color}${five_int}%${RESET}${DIM} (${five_rel})${RESET}"
    302fi
    303
    304# ---------------------------------------------------------------------------
    305# Segment 7: 7-day rate limit — omitted when block absent
    306# ---------------------------------------------------------------------------
    307seg_seven=""
    308if [ -n "$seven_pct" ] && [ -n "$seven_reset" ]; then
    309  seven_int=$(printf '%.0f' "$seven_pct")
    310  if [ "$seven_int" -ge 80 ]; then seven_color="$RED"; else seven_color="$YELLOW"; fi
    311  seven_rel=$(rel_time "$seven_reset")
    312  seg_seven="w: ${RESET}${seven_color}${seven_int}%${RESET}${DIM} (${seven_rel})${RESET}"
    313fi
    314
    315# ---------------------------------------------------------------------------
    316# Assemble output
    317# ---------------------------------------------------------------------------
    318_join() {
    319  local _ref="$1" seg="$2"
    320  [ -z "$seg" ] && return
    321  local cur
    322  eval "cur=\$$_ref"
    323  if [ -z "$cur" ]; then
    324    eval "$_ref=\$seg"
    325  else
    326    eval "$_ref=\"\${cur}\${SEP}\${seg}\""
    327  fi
    328}
    329
    330line1=""
    331_join line1 "$seg_cwd"
    332_join line1 "$seg_branch"
    333
    334line2=""
    335_join line2 "$seg_account"
    336_join line2 "$seg_model"
    337
    338line3=""
    339_join line3 "$seg_ctx"
    340_join line3 "$seg_five"
    341_join line3 "$seg_seven"
    342
    343out=""
    344for ln in "$line1" "$line2" "$line3"; do
    345  [ -z "$ln" ] && continue
    346  if [ -z "$out" ]; then
    347    out="$ln"
    348  else
    349    out="$out
    350$ln"
    351  fi
    352done
    353[ -n "$out" ] && printf "%b\n" "$out"
    
  • statusline.ps1: a PowerShell port for anyone running Claude Code on Windows from cmd.exe or PowerShell. It also runs fine under PowerShell 7+ on macOS and Linux if that’s your shell of choice.

    click to expand and see the statusline for Windows cmd.exe or PowerShell consoles
      1# ~/.claude/statusline.ps1
      2# Claude Code statusLine script — PowerShell port of statusline.sh
      3# Works under Windows PowerShell 5.1 and PowerShell 7+ (pwsh) on any OS.
      4# Configure in settings.json:
      5#   "command": "powershell -NoProfile -File ~/.claude/statusline.ps1"
      6#   (use "pwsh" instead of "powershell" on macOS/Linux)
      7#
      8# Toggle: set CLAUDE_STATUSLINE_BAR=0 to hide the context bar (keep just %)
      9# Line 1: <cwd> | [branch]
     10# Line 2: [email |] <model-id> [ctx-size]
     11# Line 3: <ctxbar> <ctx%> [tok] | [s:<5h%> (<rel>)] | [w:<7d%> (<rel>)]
     12
     13$ErrorActionPreference = 'SilentlyContinue'
     14
     15# ---------------------------------------------------------------------------
     16# ANSI colors
     17# ---------------------------------------------------------------------------
     18$ESC   = [char]27
     19$RESET  = "$ESC[0m"
     20$GREEN  = "$ESC[32m"
     21$YELLOW = "$ESC[33m"
     22$BLUE   = "$ESC[34m"
     23$RED    = "$ESC[31m"
     24$DIM    = "$ESC[2m"
     25$SEP    = "$DIM | $RESET"
     26
     27# Truecolor (24-bit). Falls back gracefully on terminals that ignore it.
     28function Rgb([int]$r, [int]$g, [int]$b) { "$ESC[38;2;$r;$g;${b}m" }
     29
     30# Toggles
     31$SHOW_BAR  = if ($env:CLAUDE_STATUSLINE_BAR) { $env:CLAUDE_STATUSLINE_BAR } else { '1' }
     32$BAR_WIDTH = if ($env:CLAUDE_STATUSLINE_BAR_WIDTH) { [int]$env:CLAUDE_STATUSLINE_BAR_WIDTH } else { 10 }
     33
     34# ---------------------------------------------------------------------------
     35# Read all fields. PowerShell parses JSON natively, so no jq dependency.
     36# ---------------------------------------------------------------------------
     37$raw = [Console]::In.ReadToEnd()
     38$data = $null
     39if ($raw) { $data = $raw | ConvertFrom-Json }
     40
     41# Helper: safely read a nested property path, returning $null if any hop is absent.
     42function Get-Field($obj, [string[]]$path) {
     43  $cur = $obj
     44  foreach ($p in $path) {
     45    if ($null -eq $cur) { return $null }
     46    $cur = $cur.PSObject.Properties[$p].Value
     47  }
     48  return $cur
     49}
     50
     51$cwd        = Get-Field $data @('workspace','current_dir'); if (-not $cwd) { $cwd = Get-Field $data @('cwd') }
     52$model_id   = Get-Field $data @('model','id')
     53$ctx_pct    = Get-Field $data @('context_window','used_percentage')
     54$in_tok     = Get-Field $data @('context_window','total_input_tokens')
     55$out_tok    = Get-Field $data @('context_window','total_output_tokens')
     56$ctx_size   = Get-Field $data @('context_window','context_window_size')
     57$five_pct   = Get-Field $data @('rate_limits','five_hour','used_percentage')
     58$five_reset = Get-Field $data @('rate_limits','five_hour','resets_at')
     59$seven_pct  = Get-Field $data @('rate_limits','seven_day','used_percentage')
     60$seven_reset= Get-Field $data @('rate_limits','seven_day','resets_at')
     61
     62if (-not $cwd) { $cwd = (Get-Location).Path }
     63
     64# ---------------------------------------------------------------------------
     65# Relative-time helper: epoch seconds -> "Xd Yh" / "Xh Ym" / "Xm Ys" / "Xs" / "now"
     66# ---------------------------------------------------------------------------
     67function Rel-Time([long]$epoch) {
     68  $now  = [int][double]::Parse((Get-Date -UFormat %s))
     69  $diff = $epoch - $now
     70  if ($diff -le 0) { return 'now' }
     71  $days  = [math]::Floor($diff / 86400)
     72  $hours = [math]::Floor(($diff % 86400) / 3600)
     73  $mins  = [math]::Floor(($diff % 3600) / 60)
     74  $secs  = $diff % 60
     75  if     ($days  -gt 0) { return "{0}d {1}h" -f $days,  $hours }
     76  elseif ($hours -gt 0) { return "{0}h {1}m" -f $hours, $mins }
     77  elseif ($mins  -gt 0) { return "{0}m {1}s" -f $mins,  $secs }
     78  else                  { return "{0}s" -f $secs }
     79}
     80
     81# ---------------------------------------------------------------------------
     82# Token formatter: 15234 -> 15.2k, 1500000 -> 1.5M
     83# ---------------------------------------------------------------------------
     84function Format-Tokens([double]$n) {
     85  if ($n -le 0)            { return '' }
     86  elseif ($n -ge 1000000) { return ('{0:0.0}M' -f ($n / 1000000)) }
     87  elseif ($n -ge 1000)    { return ('{0:0.0}k' -f ($n / 1000)) }
     88  else                    { return ('{0:0}' -f $n) }
     89}
     90
     91# ---------------------------------------------------------------------------
     92# Context-size formatter: always thousands with comma grouping
     93#   1000000 -> 1,000k   200000 -> 200k   128000 -> 128k
     94# ---------------------------------------------------------------------------
     95function Format-CtxSize([double]$n) {
     96  if ($n -le 0) { return '' }
     97  $k = [math]::Floor(($n + 500) / 1000)
     98  return ('{0:#,0}k' -f $k)
     99}
    100
    101# ---------------------------------------------------------------------------
    102# Segment 1: abbreviated cwd (blue)
    103# ---------------------------------------------------------------------------
    104$homeDir = $env:HOME; if (-not $homeDir) { $homeDir = $env:USERPROFILE }
    105$short_cwd = $cwd
    106if ($homeDir -and $cwd.StartsWith($homeDir)) {
    107  $short_cwd = '~' + $cwd.Substring($homeDir.Length)
    108}
    109$seg_cwd = "$BLUE$short_cwd$RESET"
    110
    111# ---------------------------------------------------------------------------
    112# Segment 2: git branch (green) — omitted when not in a repo
    113# ---------------------------------------------------------------------------
    114$seg_branch = ''
    115$git_root = git -C "$cwd" --no-optional-locks rev-parse --show-toplevel 2>$null
    116if ($git_root) {
    117  $branch = git -C "$cwd" --no-optional-locks symbolic-ref --short HEAD 2>$null
    118  if (-not $branch) {
    119    $sha = git -C "$cwd" --no-optional-locks rev-parse --short HEAD 2>$null
    120    $branch = "($sha)"
    121  }
    122  if ($branch) { $seg_branch = "$GREEN$branch$RESET" }
    123}
    124
    125# ---------------------------------------------------------------------------
    126# Segment 3: Claude Code authenticated account email (blue) — fail-silent
    127# Source: `claude auth status` (JSON .email), cached for 5 min.
    128# Override: set $env:CLAUDE_ACCOUNT_EMAIL to skip the CLI.
    129# ---------------------------------------------------------------------------
    130$account_email = ''
    131$tmp = if ($env:TMPDIR) { $env:TMPDIR } elseif ($env:TEMP) { $env:TEMP } else { '/tmp' }
    132$cache_file = Join-Path $tmp 'statusline-account'
    133$cache_ttl  = 300
    134
    135if ($env:CLAUDE_ACCOUNT_EMAIL) {
    136  $account_email = $env:CLAUDE_ACCOUNT_EMAIL
    137} else {
    138  $need_refresh = $true
    139  if (Test-Path $cache_file) {
    140    $age = ((Get-Date) - (Get-Item $cache_file).LastWriteTime).TotalSeconds
    141    if ($age -ge 0 -and $age -lt $cache_ttl) {
    142      $account_email = (Get-Content $cache_file -Raw 2>$null)
    143      if ($account_email) { $account_email = $account_email.Trim() }
    144      $need_refresh = $false
    145    }
    146  }
    147  if ($need_refresh -and (Get-Command claude -ErrorAction SilentlyContinue)) {
    148    $auth_json = claude auth status 2>$null | Out-String
    149    if ($auth_json) {
    150      try { $account_email = ($auth_json | ConvertFrom-Json).email } catch { $account_email = '' }
    151    }
    152    if (-not $account_email) {
    153      $txt = claude auth status --text 2>$null | Out-String
    154      $m = [regex]::Match($txt, '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}')
    155      if ($m.Success) { $account_email = $m.Value }
    156    }
    157    if ($account_email -eq 'null') { $account_email = '' }
    158    try { [IO.File]::WriteAllText($cache_file, [string]$account_email) } catch {}
    159  }
    160}
    161
    162$seg_account = if ($account_email) { "$BLUE$account_email$RESET" } else { '' }
    163
    164# ---------------------------------------------------------------------------
    165# Context bar: RGB gradient green->yellow->red
    166# ---------------------------------------------------------------------------
    167function Build-CtxBar([int]$pct_int) {
    168  $filled = [math]::Floor(($pct_int * $BAR_WIDTH + 50) / 100)
    169  $out = ''
    170  for ($i = 0; $i -lt $BAR_WIDTH; $i++) {
    171    if ($BAR_WIDTH -gt 1) { $pos = [math]::Floor($i * 100 / ($BAR_WIDTH - 1)) } else { $pos = 0 }
    172    if ($pos -le 50) {
    173      $r = [math]::Floor(220 * $pos / 50); $g = 200; $b = [math]::Floor(80 - 80 * $pos / 50)
    174    } else {
    175      $adj = $pos - 50; $r = 220; $g = [math]::Floor(200 - 160 * $adj / 50); $b = [math]::Floor(20 * $adj / 50)
    176    }
    177    if ($i -lt $filled) {
    178      $out += (Rgb $r $g $b) + [char]0x2588   # full block
    179    } else {
    180      $out += "$ESC[38;2;60;60;60m" + [char]0x2591  # light shade
    181    }
    182  }
    183  return "$out$RESET"
    184}
    185
    186# Context usage: percent, color/emoji, bar — computed first so segments 4 & 5 reuse them.
    187$ctx_int = $null
    188$bar = ''
    189$ctx_color = $GREEN
    190$ctx_emoji = ''
    191if ($null -ne $ctx_pct -and $ctx_pct -ne '') {
    192  $ctx_int = [int][math]::Round([double]$ctx_pct)
    193  if ($ctx_int -lt 0)   { $ctx_int = 0 }
    194  if ($ctx_int -gt 100) { $ctx_int = 100 }
    195
    196  if     ($ctx_int -ge 90) { $ctx_color = $RED    }
    197  elseif ($ctx_int -ge 70) { $ctx_color = $YELLOW }
    198  elseif ($ctx_int -ge 20) { $ctx_color = $GREEN  }
    199  else                     { $ctx_color = $GREEN  }
    200
    201  if ($SHOW_BAR -eq '1') { $bar = Build-CtxBar $ctx_int }
    202}
    203
    204# Segment 4: model id + total context size
    205$ctx_size_fmt = ''
    206if ($ctx_size -and [double]$ctx_size -gt 0) { $ctx_size_fmt = Format-CtxSize ([double]$ctx_size) }
    207if ($ctx_size_fmt) {
    208  $seg_model = "$model_id$SEP" + "ctx: $DIM$ctx_size_fmt$RESET"
    209} else {
    210  $seg_model = "$model_id$RESET"
    211}
    212
    213# Segment 5: context bar + context % + session tokens
    214if ($null -ne $ctx_int) {
    215  if ($bar) {
    216    $seg_ctx = "$bar $ctx_color$ctx_int%$RESET"
    217  } else {
    218    $seg_ctx = "$ctx_color$ctx_int%$RESET"
    219  }
    220} else {
    221  $seg_ctx = "--$RESET"
    222}
    223
    224# Session token total (input + output)
    225if (($null -ne $in_tok -and $in_tok -ne '') -or ($null -ne $out_tok -and $out_tok -ne '')) {
    226  $sum = 0.0
    227  if ($in_tok)  { $sum += [double]$in_tok }
    228  if ($out_tok) { $sum += [double]$out_tok }
    229  $tok_fmt = Format-Tokens $sum
    230  if ($tok_fmt) { $seg_ctx = "$seg_ctx $DIM($tok_fmt tok)$RESET" }
    231}
    232
    233# ---------------------------------------------------------------------------
    234# Segment 6: 5-hour rate limit — omitted when block absent
    235# ---------------------------------------------------------------------------
    236$seg_five = ''
    237if (($null -ne $five_pct -and $five_pct -ne '') -and ($null -ne $five_reset -and $five_reset -ne '')) {
    238  $five_int = [int][math]::Round([double]$five_pct)
    239  $five_color = if ($five_int -ge 80) { $RED } else { $YELLOW }
    240  $five_rel = Rel-Time ([long]$five_reset)
    241  $seg_five = "s: $RESET$five_color$five_int%$RESET$DIM ($five_rel)$RESET"
    242}
    243
    244# ---------------------------------------------------------------------------
    245# Segment 7: 7-day rate limit — omitted when block absent
    246# ---------------------------------------------------------------------------
    247$seg_seven = ''
    248if (($null -ne $seven_pct -and $seven_pct -ne '') -and ($null -ne $seven_reset -and $seven_reset -ne '')) {
    249  $seven_int = [int][math]::Round([double]$seven_pct)
    250  $seven_color = if ($seven_int -ge 80) { $RED } else { $YELLOW }
    251  $seven_rel = Rel-Time ([long]$seven_reset)
    252  $seg_seven = "w: $RESET$seven_color$seven_int%$RESET$DIM ($seven_rel)$RESET"
    253}
    254
    255# ---------------------------------------------------------------------------
    256# Assemble output
    257# ---------------------------------------------------------------------------
    258function Join-Segs([string[]]$segs) {
    259  ($segs | Where-Object { $_ -ne '' -and $null -ne $_ }) -join $SEP
    260}
    261
    262$line1 = Join-Segs @($seg_cwd, $seg_branch)
    263$line2 = Join-Segs @($seg_account, $seg_model)
    264$line3 = Join-Segs @($seg_ctx, $seg_five, $seg_seven)
    265
    266$lines = @($line1, $line2, $line3) | Where-Object { $_ -ne '' }
    267[Console]::Out.Write(($lines -join "`n") + "`n")

I’m not going to walk through every line of code because you don’t need to understand it to use it, but the flow is straightforward. Claude Code pipes a JSON payload to the script on stdin after every turn. The script pulls out the fields it cares about, including the working directory, model ID, context window usage, and the 5-hour and 7-day rate limit blocks, then formats them into the three lines you saw above with ANSI colors and the gradient progress bar.

The bash version uses jq to parse the JSON when it’s available and falls back to a POSIX grep/sed parser when it isn’t, so it still renders on a fresh Git Bash install. The PowerShell version parses JSON natively with ConvertFrom-Json, so it has no external dependencies at all.

Both scripts also support a couple of environment variables for tweaking the display, like CLAUDE_STATUSLINE_BAR=0 to hide the progress bar and keep just the percentage, or CLAUDE_STATUSLINE_BAR_WIDTH to change the bar’s width.

If you want to build your own version, here’s the full set of fields Claude Code sends in the JSON payload on stdin:

FieldTypeDescription
workspace.current_dirstringAbsolute path to the current working directory
model.idstringFull model ID (e.g., claude-opus-4-8[1m])
context_window.used_percentagenumberContext window fill as a percentage (0–100)
context_window.total_input_tokensnumberInput tokens consumed this session
context_window.total_output_tokensnumberOutput tokens generated this session
context_window.context_window_sizenumberMaximum token capacity of the context window
rate_limits.five_hour.used_percentagenumber5-hour session limit consumed (0–100)
rate_limits.five_hour.resets_atnumberUnix timestamp of the 5-hour limit reset
rate_limits.seven_day.used_percentagenumber7-day rolling quota consumed (0–100)
rate_limits.seven_day.resets_atnumberUnix timestamp of the 7-day limit reset

Install it on macOS, Linux, WSL, or Git Bash

If you’re on macOS or Linux, or on Windows using WSL or Git Bash as your shell, you’ll use the bash script:

  1. Copy statusline.sh to ~/.claude/statusline.sh.

  2. Make sure it has the correct permissions applied, by running the following:

    1chmod +x ~/.claude/statusline.sh
    
  3. Open ~/.claude/settings.json and add the following:

    1"statusLine": {
    2  "type": "command",
    3  "command": "~/.claude/statusline.sh"
    4}

That’s it. The next turn you take in Claude Code, the status line renders. If you don’t have jq installed, the script still works in fallback mode, but I’d recommend installing it for the most reliable parsing:

1brew install jq

Install it on Windows with cmd.exe or PowerShell

If you’re on Windows running Claude Code from cmd.exe or PowerShell, use the PowerShell script:

  1. Copy statusline.ps1 to ~/.claude/statusline.ps1.

  2. Open ~/.claude/settings.json and add the following:

    1"statusLine": {
    2  "type": "command",
    3  "command": "powershell -NoProfile -File $HOME/.claude/statusline.ps1"
    4}
Using PowerShell 7+ on macOS or Linux?

The PowerShell script works cross-platform. Just swap powershell for pwsh in the command above if you’re using PowerShell on macOS.

Wrapping it up

Usage-based pricing changed the deal for a lot of developers, and whether you’ve moved to Claude Code or you’re managing credits in another tool, the lesson is the same: token consumption is now something you manage, not something you ignore. A status line that surfaces your context window and quota consumption after every turn turns that management from guesswork into a glance.

Grab the scripts, drop them into your ~/.claude folder, and add three lines to your settings. Five minutes of setup, and you’ll never wonder where your tokens went again.

How are you keeping tabs on your token consumption? Have you customized your status line, or are you flying blind? I’d love to hear what you’re tracking in the comments below.

Share this article

Feedback & comments