Voitanos is Closing October 1, 2026
Read the announcement, how it impacts customers, and 50% off everything!
Read The Announcement
articles

Customize Your Claude Code Status Line to Manage Token Burn

Customize the Claude Code status line with a script that shows context window fill, 5-hour session limits, and 7-day quota after every turn. Works on macOS, Linux, and Windows.

Customize Your Claude Code Status Line to Manage Token Burn
by Andrew Connell

Last updated June 6, 2026
24 minutes read

Share this

Focus Mode

  • Why the status line is worth customizing
  • What the Claude Code status line displays
  • How to use context window data to manage token consumption
  • How the Claude Code status line script works
  • Install it on macOS, Linux, WSL, or Git Bash
  • Install it on Windows with cmd.exe or PowerShell
  • Wrapping it up
  • Feedback & Comments

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
    #!/usr/bin/env bash
    # ~/.claude/statusline.sh
    # Claude Code statusLine script
    # Toggle: set CLAUDE_STATUSLINE_BAR=0 to hide the context bar (keep just %)
    # Line 1: <cwd> | [branch]
    # Line 2: [email |] <model-id> [ctx-size]
    # Line 3: <ctxbar> <ctx%> [tok] | [s:<5h%> (<rel>)] | [w:<7d%> (<rel>)]
    
    # ---------------------------------------------------------------------------
    # ANSI colors
    # ---------------------------------------------------------------------------
    RESET='\033[0m'
    GREEN='\033[32m'
    YELLOW='\033[33m'
    BLUE='\033[34m'
    RED='\033[31m'
    DIM='\033[2m'
    
    SEP="${DIM} | ${RESET}"
    
    # Truecolor helper (24-bit). Falls back gracefully on terminals that ignore it.
    rgb() { printf '\033[38;2;%d;%d;%dm' "$1" "$2" "$3"; }
    
    # Toggle: set CLAUDE_STATUSLINE_BAR=0 to hide the context bar (keep just %)
    SHOW_BAR="${CLAUDE_STATUSLINE_BAR:-1}"
    BAR_WIDTH="${CLAUDE_STATUSLINE_BAR_WIDTH:-10}"
    
    # ---------------------------------------------------------------------------
    # Read all fields. Prefer jq; fall back to a grep/sed parser when jq is absent
    # (e.g. a fresh Windows Git Bash install) so the statusline still renders.
    # ---------------------------------------------------------------------------
    input=$(cat)
    
    # Use ASCII Unit Separator (0x1F) as the field delimiter. Unlike tab/space,
    # bash does NOT collapse consecutive non-whitespace IFS chars, so empty fields
    # (e.g. absent cwd or rate_limits) are preserved and columns never shift.
    _US=$'\x1f'
    
    if command -v jq >/dev/null 2>&1; then
    _fields=$(printf '%s' "$input" | jq -j '[
      (.workspace.current_dir // .cwd // ""),
      (.model.id // ""),
      (.context_window.used_percentage | if . != null then tostring else "" end),
      (.context_window.total_input_tokens | if . != null then tostring else "" end),
      (.context_window.total_output_tokens | if . != null then tostring else "" end),
      (.context_window.context_window_size | if . != null then tostring else "" end),
      (.rate_limits.five_hour.used_percentage | if . != null then tostring else "" end),
      (.rate_limits.five_hour.resets_at | if . != null then tostring else "" end),
      (.rate_limits.seven_day.used_percentage | if . != null then tostring else "" end),
      (.rate_limits.seven_day.resets_at | if . != null then tostring else "" end)
    ] | join("\u001f")' 2>/dev/null)
    
    IFS="$_US" read -r cwd model_id ctx_pct in_tok out_tok ctx_size five_pct five_reset seven_pct seven_reset <<EOF
    $_fields
    EOF
    else
      # ---- jq-less fallback -----------------------------------------------------
      # Pull scalar fields with grep/sed (POSIX tools present on macOS & Git Bash).
      # Strings: capture "key":"value" ; Numbers: capture "key":<number>.
      # Nested keys are disambiguated by slicing to the relevant object first.
      # Flatten newlines first so slicing works on pretty-printed JSON too.
      _flat=$(printf '%s' "$input" | tr '\n' ' ')
      _jstr() { printf '%s' "$1" | grep -Eo "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -n1 | sed -E "s/.*:[[:space:]]*\"([^\"]*)\"/\1/"; }
      _jnum() { printf '%s' "$1" | grep -Eo "\"$2\"[[:space:]]*:[[:space:]]*-?[0-9]+(\.[0-9]+)?" | head -n1 | sed -E "s/.*:[[:space:]]*(-?[0-9.]+)/\1/"; }
      # Crude object slicer: drop everything up to and including "key": { (single line in).
      _jobj() { printf '%s' "$1" | sed -E "s/.*\"$2\"[[:space:]]*:[[:space:]]*\{//"; }
    
      cwd=$(_jstr "$_flat" "current_dir"); [ -z "$cwd" ] && cwd=$(_jstr "$_flat" "cwd")
      model_id=$(_jstr "$_flat" "id")
      _cw=$(_jobj "$_flat" "context_window")
      ctx_pct=$(_jnum "$_cw" "used_percentage")
      in_tok=$(_jnum "$_cw" "total_input_tokens")
      out_tok=$(_jnum "$_cw" "total_output_tokens")
      ctx_size=$(_jnum "$_cw" "context_window_size")
      _5h=$(_jobj "$_flat" "five_hour")
      five_pct=$(_jnum "$_5h" "used_percentage")
      five_reset=$(_jnum "$_5h" "resets_at")
      _7d=$(_jobj "$_flat" "seven_day")
      seven_pct=$(_jnum "$_7d" "used_percentage")
      seven_reset=$(_jnum "$_7d" "resets_at")
    
      # One-time install hint (cached so it nags at most once per TTL window).
      _jq_hint_file="${TMPDIR:-/tmp}/statusline-jq-hint"
      if [ ! -f "$_jq_hint_file" ]; then
        case "$(uname -s 2>/dev/null)" in
          Darwin*)              _jq_install="brew install jq" ;;
          MINGW*|MSYS*|CYGWIN*) _jq_install="winget install jqlang.jq   (or: choco install jq / scoop install jq)" ;;
          Linux*)               _jq_install="sudo apt install jq   (or your distro's package manager)" ;;
          *)                    _jq_install="see https://jqlang.github.io/jq/download/" ;;
        esac
        printf 'claude statusline: jq not found — limited output. Install: %s\n' "$_jq_install" >&2
        : > "$_jq_hint_file" 2>/dev/null
      fi
    fi
    
    [ -z "$cwd" ] && cwd="$PWD"
    
    # ---------------------------------------------------------------------------
    # Relative-time helper: seconds → "Xd Yh" / "Xh Ym" / "Xm Ys" / "Xs" / "now"
    # ---------------------------------------------------------------------------
    rel_time() {
      local epoch="$1"
      local now
      now=$(date +%s)
      local diff=$(( epoch - now ))
      if [ "$diff" -le 0 ]; then
        printf 'now'
        return
      fi
      local days=$(( diff / 86400 ))
      local hours=$(( (diff % 86400) / 3600 ))
      local mins=$(( (diff % 3600) / 60 ))
      local secs=$(( diff % 60 ))
      if   [ "$days"  -gt 0 ]; then printf '%dd %dh' "$days"  "$hours"
      elif [ "$hours" -gt 0 ]; then printf '%dh %dm' "$hours" "$mins"
      elif [ "$mins"  -gt 0 ]; then printf '%dm %ds' "$mins"  "$secs"
      else                           printf '%ds'     "$secs"
      fi
    }
    
    # ---------------------------------------------------------------------------
    # Token formatter: 15234 → 15.2k, 1500000 → 1.5M
    # ---------------------------------------------------------------------------
    fmt_tokens() {
      awk -v n="${1:-0}" 'BEGIN {
        if (n+0 <= 0)            { printf ""; }
        else if (n >= 1000000)   { printf "%.1fM", n/1000000 }
        else if (n >= 1000)      { printf "%.1fk", n/1000 }
        else                     { printf "%d", n }
      }'
    }
    
    # ---------------------------------------------------------------------------
    # Context-size formatter: always thousands with comma grouping
    #   1000000 → 1,000k   200000 → 200k   128000 → 128k
    # ---------------------------------------------------------------------------
    fmt_ctx_size() {
      awk -v n="${1:-0}" 'BEGIN {
        if (n+0 <= 0) { printf ""; exit }
        k = int((n + 500) / 1000)          # round to nearest thousand
        s = sprintf("%d", k)
        out = ""
        len = length(s)
        for (i = 1; i <= len; i++) {
          out = out substr(s, i, 1)
          rem = len - i
          if (rem > 0 && rem % 3 == 0) out = out ","
        }
        printf "%sk", out
      }'
    }
    
    # ---------------------------------------------------------------------------
    # Segment 1: abbreviated cwd (blue)
    # ---------------------------------------------------------------------------
    short_cwd="${cwd/#"$HOME"/~}"
    seg_cwd="${BLUE}${short_cwd}${RESET}"
    
    # ---------------------------------------------------------------------------
    # Segment 2: git branch (green) — omitted when not in a repo
    # ---------------------------------------------------------------------------
    seg_branch=""
    git_root=$(git -C "$cwd" --no-optional-locks rev-parse --show-toplevel 2>/dev/null)
    if [ -n "$git_root" ]; then
      branch=$(git -C "$cwd" --no-optional-locks symbolic-ref --short HEAD 2>/dev/null)
      if [ -z "$branch" ]; then
        branch=$(git -C "$cwd" --no-optional-locks rev-parse --short HEAD 2>/dev/null)
        branch="(${branch})"
      fi
      seg_branch="${GREEN}${branch}${RESET}"
    fi
    
    # ---------------------------------------------------------------------------
    # Segment 3: Claude Code authenticated account email (blue) — fail-silent
    # Source: `claude auth status` (JSON .email), cached in $TMPDIR for 5 min.
    # Override: set $CLAUDE_ACCOUNT_EMAIL in the environment to skip the CLI.
    # Force refresh: rm "${TMPDIR:-/tmp}/statusline-account"
    # ---------------------------------------------------------------------------
    account_email=""
    _cache_file="${TMPDIR:-/tmp}/statusline-account"
    _cache_ttl=300  # seconds
    
    if [ -n "$CLAUDE_ACCOUNT_EMAIL" ]; then
      account_email="$CLAUDE_ACCOUNT_EMAIL"
    else
      _need_refresh=1
      if [ -f "$_cache_file" ]; then
        _mtime=$(stat -f %m "$_cache_file" 2>/dev/null || stat -c %Y "$_cache_file" 2>/dev/null || echo 0)
        _cache_age=$(( $(date +%s) - _mtime ))
        if [ "$_cache_age" -ge 0 ] && [ "$_cache_age" -lt "$_cache_ttl" ]; then
          account_email=$(cat "$_cache_file" 2>/dev/null)
          _need_refresh=
        fi
      fi
    
      if [ -n "$_need_refresh" ] && command -v claude >/dev/null 2>&1; then
        _auth_json=$(claude auth status 2>/dev/null)
        if [ -n "$_auth_json" ]; then
          account_email=$(printf '%s' "$_auth_json" | jq -r '.email // empty' 2>/dev/null)
        fi
        if [ -z "$account_email" ] || [ "$account_email" = "null" ]; then
          account_email=$(claude auth status --text 2>/dev/null \
            | grep -Eo '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' \
            | head -n1)
        fi
        [ "$account_email" = "null" ] && account_email=""
        printf '%s' "$account_email" > "$_cache_file" 2>/dev/null
      fi
    fi
    
    if [ -n "$account_email" ]; then
      seg_account="${BLUE}${account_email}${RESET}"
    else
      seg_account=""
    fi
    
    # ---------------------------------------------------------------------------
    # Segment 4: model id + total context size (size in thousands, e.g. 1,000k)
    # Segment 5: context bar + context % + session tokens
    #   bar: RGB gradient green→yellow→red, dynamic emoji, % colored by threshold
    #   thresholds: <70 green, 70–89 yellow, >=90 red (emoji adds 20% step)
    # ---------------------------------------------------------------------------
    build_ctx_bar() {
      local pct_int="$1" filled i pos r g b out=""
      filled=$(( (pct_int * BAR_WIDTH + 50) / 100 ))
      for (( i=0; i<BAR_WIDTH; i++ )); do
        if [ "$BAR_WIDTH" -gt 1 ]; then pos=$(( i * 100 / (BAR_WIDTH - 1) )); else pos=0; fi
        if [ "$pos" -le 50 ]; then
          r=$(( 0 + 220 * pos / 50 )); g=200; b=$(( 80 - 80 * pos / 50 ))
        else
          local adj=$(( pos - 50 )); r=220; g=$(( 200 - 160 * adj / 50 )); b=$(( 0 + 20 * adj / 50 ))
        fi
        if [ "$i" -lt "$filled" ]; then
          out="${out}$(rgb $r $g $b)█"
        else
          out="${out}\033[38;2;60;60;60m░"
        fi
      done
      printf '%b' "${out}${RESET}"
    }
    
    # Context usage: percent, color/emoji, and bar — computed first so both
    # segment 4 (model line) and segment 5 (context line) can reuse them.
    ctx_int=""
    bar=""
    if [ -n "$ctx_pct" ]; then
      ctx_int=$(printf '%.0f' "$ctx_pct")
      [ "$ctx_int" -lt 0 ] && ctx_int=0
      [ "$ctx_int" -gt 100 ] && ctx_int=100
    
      if   [ "$ctx_int" -ge 90 ]; then ctx_color="$RED";
      elif [ "$ctx_int" -ge 70 ]; then ctx_color="$YELLOW";
      elif [ "$ctx_int" -ge 20 ]; then ctx_color="$GREEN";
      else                             ctx_color="$GREEN";
      fi
    
      [ "$SHOW_BAR" = "1" ] && bar=$(build_ctx_bar "$ctx_int")
    fi
    
    # Segment 4: model id + context bar + total context size
    ctx_size_fmt=""
    if [ -n "$ctx_size" ] && [ "${ctx_size:-0}" -gt 0 ]; then
      ctx_size_fmt=$(fmt_ctx_size "$ctx_size")
    fi
    if [ -n "$ctx_size_fmt" ]; then
      seg_model="${model_id}${SEP}ctx: ${DIM}${ctx_size_fmt}${RESET}"
    else
      seg_model="${model_id}${RESET}"
    fi
    
    # Segment 5: context bar + context % + session tokens
    if [ -n "$ctx_int" ]; then
      if [ -n "$bar" ]; then
        seg_ctx="${bar} ${ctx_color}${ctx_int}%${RESET}"
      else
        seg_ctx="${ctx_color}${ctx_int}%${RESET}"
      fi
    else
      seg_ctx="--${RESET}"
    fi
    
    # Session token total (input + output), appended to the context segment when present
    tok_total=""
    if [ -n "$in_tok" ] || [ -n "$out_tok" ]; then
      sum=$(( ${in_tok:-0} + ${out_tok:-0} ))
      tok_fmt=$(fmt_tokens "$sum")
      if [ -n "$tok_fmt" ]; then
        tok_total="${DIM}(${tok_fmt} tok)${RESET}"
        seg_ctx="${seg_ctx} ${tok_total}"
      fi
    fi
    
    # ---------------------------------------------------------------------------
    # Segment 6: 5-hour rate limit — omitted when block absent
    # ---------------------------------------------------------------------------
    seg_five=""
    if [ -n "$five_pct" ] && [ -n "$five_reset" ]; then
      five_int=$(printf '%.0f' "$five_pct")
      if [ "$five_int" -ge 80 ]; then five_color="$RED"; else five_color="$YELLOW"; fi
      five_rel=$(rel_time "$five_reset")
      seg_five="s: ${RESET}${five_color}${five_int}%${RESET}${DIM} (${five_rel})${RESET}"
    fi
    
    # ---------------------------------------------------------------------------
    # Segment 7: 7-day rate limit — omitted when block absent
    # ---------------------------------------------------------------------------
    seg_seven=""
    if [ -n "$seven_pct" ] && [ -n "$seven_reset" ]; then
      seven_int=$(printf '%.0f' "$seven_pct")
      if [ "$seven_int" -ge 80 ]; then seven_color="$RED"; else seven_color="$YELLOW"; fi
      seven_rel=$(rel_time "$seven_reset")
      seg_seven="w: ${RESET}${seven_color}${seven_int}%${RESET}${DIM} (${seven_rel})${RESET}"
    fi
    
    # ---------------------------------------------------------------------------
    # Assemble output
    # ---------------------------------------------------------------------------
    _join() {
      local _ref="$1" seg="$2"
      [ -z "$seg" ] && return
      local cur
      eval "cur=\$$_ref"
      if [ -z "$cur" ]; then
        eval "$_ref=\$seg"
      else
        eval "$_ref=\"\${cur}\${SEP}\${seg}\""
      fi
    }
    
    line1=""
    _join line1 "$seg_cwd"
    _join line1 "$seg_branch"
    
    line2=""
    _join line2 "$seg_account"
    _join line2 "$seg_model"
    
    line3=""
    _join line3 "$seg_ctx"
    _join line3 "$seg_five"
    _join line3 "$seg_seven"
    
    out=""
    for ln in "$line1" "$line2" "$line3"; do
      [ -z "$ln" ] && continue
      if [ -z "$out" ]; then
        out="$ln"
      else
        out="$out
    $ln"
      fi
    done
    [ -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
    # ~/.claude/statusline.ps1
    # Claude Code statusLine script — PowerShell port of statusline.sh
    # Works under Windows PowerShell 5.1 and PowerShell 7+ (pwsh) on any OS.
    # Configure in settings.json:
    #   "command": "powershell -NoProfile -File ~/.claude/statusline.ps1"
    #   (use "pwsh" instead of "powershell" on macOS/Linux)
    #
    # Toggle: set CLAUDE_STATUSLINE_BAR=0 to hide the context bar (keep just %)
    # Line 1: <cwd> | [branch]
    # Line 2: [email |] <model-id> [ctx-size]
    # Line 3: <ctxbar> <ctx%> [tok] | [s:<5h%> (<rel>)] | [w:<7d%> (<rel>)]
    
    $ErrorActionPreference = 'SilentlyContinue'
    
    # ---------------------------------------------------------------------------
    # ANSI colors
    # ---------------------------------------------------------------------------
    $ESC   = [char]27
    $RESET  = "$ESC[0m"
    $GREEN  = "$ESC[32m"
    $YELLOW = "$ESC[33m"
    $BLUE   = "$ESC[34m"
    $RED    = "$ESC[31m"
    $DIM    = "$ESC[2m"
    $SEP    = "$DIM | $RESET"
    
    # Truecolor (24-bit). Falls back gracefully on terminals that ignore it.
    function Rgb([int]$r, [int]$g, [int]$b) { "$ESC[38;2;$r;$g;${b}m" }
    
    # Toggles
    $SHOW_BAR  = if ($env:CLAUDE_STATUSLINE_BAR) { $env:CLAUDE_STATUSLINE_BAR } else { '1' }
    $BAR_WIDTH = if ($env:CLAUDE_STATUSLINE_BAR_WIDTH) { [int]$env:CLAUDE_STATUSLINE_BAR_WIDTH } else { 10 }
    
    # ---------------------------------------------------------------------------
    # Read all fields. PowerShell parses JSON natively, so no jq dependency.
    # ---------------------------------------------------------------------------
    $raw = [Console]::In.ReadToEnd()
    $data = $null
    if ($raw) { $data = $raw | ConvertFrom-Json }
    
    # Helper: safely read a nested property path, returning $null if any hop is absent.
    function Get-Field($obj, [string[]]$path) {
      $cur = $obj
      foreach ($p in $path) {
        if ($null -eq $cur) { return $null }
        $cur = $cur.PSObject.Properties[$p].Value
      }
      return $cur
    }
    
    $cwd        = Get-Field $data @('workspace','current_dir'); if (-not $cwd) { $cwd = Get-Field $data @('cwd') }
    $model_id   = Get-Field $data @('model','id')
    $ctx_pct    = Get-Field $data @('context_window','used_percentage')
    $in_tok     = Get-Field $data @('context_window','total_input_tokens')
    $out_tok    = Get-Field $data @('context_window','total_output_tokens')
    $ctx_size   = Get-Field $data @('context_window','context_window_size')
    $five_pct   = Get-Field $data @('rate_limits','five_hour','used_percentage')
    $five_reset = Get-Field $data @('rate_limits','five_hour','resets_at')
    $seven_pct  = Get-Field $data @('rate_limits','seven_day','used_percentage')
    $seven_reset= Get-Field $data @('rate_limits','seven_day','resets_at')
    
    if (-not $cwd) { $cwd = (Get-Location).Path }
    
    # ---------------------------------------------------------------------------
    # Relative-time helper: epoch seconds -> "Xd Yh" / "Xh Ym" / "Xm Ys" / "Xs" / "now"
    # ---------------------------------------------------------------------------
    function Rel-Time([long]$epoch) {
      $now  = [int][double]::Parse((Get-Date -UFormat %s))
      $diff = $epoch - $now
      if ($diff -le 0) { return 'now' }
      $days  = [math]::Floor($diff / 86400)
      $hours = [math]::Floor(($diff % 86400) / 3600)
      $mins  = [math]::Floor(($diff % 3600) / 60)
      $secs  = $diff % 60
      if     ($days  -gt 0) { return "{0}d {1}h" -f $days,  $hours }
      elseif ($hours -gt 0) { return "{0}h {1}m" -f $hours, $mins }
      elseif ($mins  -gt 0) { return "{0}m {1}s" -f $mins,  $secs }
      else                  { return "{0}s" -f $secs }
    }
    
    # ---------------------------------------------------------------------------
    # Token formatter: 15234 -> 15.2k, 1500000 -> 1.5M
    # ---------------------------------------------------------------------------
    function Format-Tokens([double]$n) {
      if ($n -le 0)            { return '' }
      elseif ($n -ge 1000000) { return ('{0:0.0}M' -f ($n / 1000000)) }
      elseif ($n -ge 1000)    { return ('{0:0.0}k' -f ($n / 1000)) }
      else                    { return ('{0:0}' -f $n) }
    }
    
    # ---------------------------------------------------------------------------
    # Context-size formatter: always thousands with comma grouping
    #   1000000 -> 1,000k   200000 -> 200k   128000 -> 128k
    # ---------------------------------------------------------------------------
    function Format-CtxSize([double]$n) {
      if ($n -le 0) { return '' }
      $k = [math]::Floor(($n + 500) / 1000)
      return ('{0:#,0}k' -f $k)
    }
    
    # ---------------------------------------------------------------------------
    # Segment 1: abbreviated cwd (blue)
    # ---------------------------------------------------------------------------
    $homeDir = $env:HOME; if (-not $homeDir) { $homeDir = $env:USERPROFILE }
    $short_cwd = $cwd
    if ($homeDir -and $cwd.StartsWith($homeDir)) {
      $short_cwd = '~' + $cwd.Substring($homeDir.Length)
    }
    $seg_cwd = "$BLUE$short_cwd$RESET"
    
    # ---------------------------------------------------------------------------
    # Segment 2: git branch (green) — omitted when not in a repo
    # ---------------------------------------------------------------------------
    $seg_branch = ''
    $git_root = git -C "$cwd" --no-optional-locks rev-parse --show-toplevel 2>$null
    if ($git_root) {
      $branch = git -C "$cwd" --no-optional-locks symbolic-ref --short HEAD 2>$null
      if (-not $branch) {
        $sha = git -C "$cwd" --no-optional-locks rev-parse --short HEAD 2>$null
        $branch = "($sha)"
      }
      if ($branch) { $seg_branch = "$GREEN$branch$RESET" }
    }
    
    # ---------------------------------------------------------------------------
    # Segment 3: Claude Code authenticated account email (blue) — fail-silent
    # Source: `claude auth status` (JSON .email), cached for 5 min.
    # Override: set $env:CLAUDE_ACCOUNT_EMAIL to skip the CLI.
    # ---------------------------------------------------------------------------
    $account_email = ''
    $tmp = if ($env:TMPDIR) { $env:TMPDIR } elseif ($env:TEMP) { $env:TEMP } else { '/tmp' }
    $cache_file = Join-Path $tmp 'statusline-account'
    $cache_ttl  = 300
    
    if ($env:CLAUDE_ACCOUNT_EMAIL) {
      $account_email = $env:CLAUDE_ACCOUNT_EMAIL
    } else {
      $need_refresh = $true
      if (Test-Path $cache_file) {
        $age = ((Get-Date) - (Get-Item $cache_file).LastWriteTime).TotalSeconds
        if ($age -ge 0 -and $age -lt $cache_ttl) {
          $account_email = (Get-Content $cache_file -Raw 2>$null)
          if ($account_email) { $account_email = $account_email.Trim() }
          $need_refresh = $false
        }
      }
      if ($need_refresh -and (Get-Command claude -ErrorAction SilentlyContinue)) {
        $auth_json = claude auth status 2>$null | Out-String
        if ($auth_json) {
          try { $account_email = ($auth_json | ConvertFrom-Json).email } catch { $account_email = '' }
        }
        if (-not $account_email) {
          $txt = claude auth status --text 2>$null | Out-String
          $m = [regex]::Match($txt, '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}')
          if ($m.Success) { $account_email = $m.Value }
        }
        if ($account_email -eq 'null') { $account_email = '' }
        try { [IO.File]::WriteAllText($cache_file, [string]$account_email) } catch {}
      }
    }
    
    $seg_account = if ($account_email) { "$BLUE$account_email$RESET" } else { '' }
    
    # ---------------------------------------------------------------------------
    # Context bar: RGB gradient green->yellow->red
    # ---------------------------------------------------------------------------
    function Build-CtxBar([int]$pct_int) {
      $filled = [math]::Floor(($pct_int * $BAR_WIDTH + 50) / 100)
      $out = ''
      for ($i = 0; $i -lt $BAR_WIDTH; $i++) {
        if ($BAR_WIDTH -gt 1) { $pos = [math]::Floor($i * 100 / ($BAR_WIDTH - 1)) } else { $pos = 0 }
        if ($pos -le 50) {
          $r = [math]::Floor(220 * $pos / 50); $g = 200; $b = [math]::Floor(80 - 80 * $pos / 50)
        } else {
          $adj = $pos - 50; $r = 220; $g = [math]::Floor(200 - 160 * $adj / 50); $b = [math]::Floor(20 * $adj / 50)
        }
        if ($i -lt $filled) {
          $out += (Rgb $r $g $b) + [char]0x2588   # full block
        } else {
          $out += "$ESC[38;2;60;60;60m" + [char]0x2591  # light shade
        }
      }
      return "$out$RESET"
    }
    
    # Context usage: percent, color/emoji, bar — computed first so segments 4 & 5 reuse them.
    $ctx_int = $null
    $bar = ''
    $ctx_color = $GREEN
    $ctx_emoji = ''
    if ($null -ne $ctx_pct -and $ctx_pct -ne '') {
      $ctx_int = [int][math]::Round([double]$ctx_pct)
      if ($ctx_int -lt 0)   { $ctx_int = 0 }
      if ($ctx_int -gt 100) { $ctx_int = 100 }
    
      if     ($ctx_int -ge 90) { $ctx_color = $RED    }
      elseif ($ctx_int -ge 70) { $ctx_color = $YELLOW }
      elseif ($ctx_int -ge 20) { $ctx_color = $GREEN  }
      else                     { $ctx_color = $GREEN  }
    
      if ($SHOW_BAR -eq '1') { $bar = Build-CtxBar $ctx_int }
    }
    
    # Segment 4: model id + total context size
    $ctx_size_fmt = ''
    if ($ctx_size -and [double]$ctx_size -gt 0) { $ctx_size_fmt = Format-CtxSize ([double]$ctx_size) }
    if ($ctx_size_fmt) {
      $seg_model = "$model_id$SEP" + "ctx: $DIM$ctx_size_fmt$RESET"
    } else {
      $seg_model = "$model_id$RESET"
    }
    
    # Segment 5: context bar + context % + session tokens
    if ($null -ne $ctx_int) {
      if ($bar) {
        $seg_ctx = "$bar $ctx_color$ctx_int%$RESET"
      } else {
        $seg_ctx = "$ctx_color$ctx_int%$RESET"
      }
    } else {
      $seg_ctx = "--$RESET"
    }
    
    # Session token total (input + output)
    if (($null -ne $in_tok -and $in_tok -ne '') -or ($null -ne $out_tok -and $out_tok -ne '')) {
      $sum = 0.0
      if ($in_tok)  { $sum += [double]$in_tok }
      if ($out_tok) { $sum += [double]$out_tok }
      $tok_fmt = Format-Tokens $sum
      if ($tok_fmt) { $seg_ctx = "$seg_ctx $DIM($tok_fmt tok)$RESET" }
    }
    
    # ---------------------------------------------------------------------------
    # Segment 6: 5-hour rate limit — omitted when block absent
    # ---------------------------------------------------------------------------
    $seg_five = ''
    if (($null -ne $five_pct -and $five_pct -ne '') -and ($null -ne $five_reset -and $five_reset -ne '')) {
      $five_int = [int][math]::Round([double]$five_pct)
      $five_color = if ($five_int -ge 80) { $RED } else { $YELLOW }
      $five_rel = Rel-Time ([long]$five_reset)
      $seg_five = "s: $RESET$five_color$five_int%$RESET$DIM ($five_rel)$RESET"
    }
    
    # ---------------------------------------------------------------------------
    # Segment 7: 7-day rate limit — omitted when block absent
    # ---------------------------------------------------------------------------
    $seg_seven = ''
    if (($null -ne $seven_pct -and $seven_pct -ne '') -and ($null -ne $seven_reset -and $seven_reset -ne '')) {
      $seven_int = [int][math]::Round([double]$seven_pct)
      $seven_color = if ($seven_int -ge 80) { $RED } else { $YELLOW }
      $seven_rel = Rel-Time ([long]$seven_reset)
      $seg_seven = "w: $RESET$seven_color$seven_int%$RESET$DIM ($seven_rel)$RESET"
    }
    
    # ---------------------------------------------------------------------------
    # Assemble output
    # ---------------------------------------------------------------------------
    function Join-Segs([string[]]$segs) {
      ($segs | Where-Object { $_ -ne '' -and $null -ne $_ }) -join $SEP
    }
    
    $line1 = Join-Segs @($seg_cwd, $seg_branch)
    $line2 = Join-Segs @($seg_account, $seg_model)
    $line3 = Join-Segs @($seg_ctx, $seg_five, $seg_seven)
    
    $lines = @($line1, $line2, $line3) | Where-Object { $_ -ne '' }
    [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:

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

    "statusLine": {
      "type": "command",
      "command": "~/.claude/statusline.sh"
    }
    

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:

brew 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:

    "statusLine": {
      "type": "command",
      "command": "powershell -NoProfile -File $HOME/.claude/statusline.ps1"
    }
    

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.

Andrew Connell, Microsoft MVP, Full-Stack Developer & Chief Course Artisan - Voitanos LLC.
author
Andrew Connell

Microsoft MVP, Full-Stack Developer & Chief Course Artisan - Voitanos LLC.

Andrew Connell is a full stack developer who focuses on Microsoft Azure & Microsoft 365. He’s a 22-year recipient of Microsoft’s MVP award and has helped thousands of developers through the various courses he’s authored & taught. Whether it’s an introduction to the entire ecosystem, or a deep dive into a specific software, his resources, tools, and support help web developers become experts in the Microsoft 365 ecosystem, so they can become irreplaceable in their organization.

Feedback & Questions

newsletter

Join 12,000+ developers for news & insights

No clickbait · 100% free · Unsubscribe anytime.

    Subscribe to Andrew's newsletter for insights & stay on top of the latest news in the Microsoft 365 Space!
    blurry dot in brand primary color
    found this article helpful?

    You'll love these!

    My Thoughts on Vibe Coding vs. Agentic Engineering

    My Thoughts on Vibe Coding vs. Agentic Engineering

    March 23, 2026

    Read now

    Replicate Your Hands, Not Your Brain: When to Automate with AI

    Replicate Your Hands, Not Your Brain: When to Automate with AI

    May 10, 2026

    Read now

    My Thoughts on Vibe Coding vs. Agentic Engineering

    My Thoughts on Vibe Coding vs. Agentic Engineering

    March 23, 2026

    Read now

    bi-weekly newsletter

    Join 12,000+ Microsoft 365 full-stack web developers for news, insights & resources. 100% free.

    Subscribe to Andrew's newsletter for insights & stay on top of the latest news in the Microsoft 365 ecosystem!

    No clickbait · 100% free · Unsubscribe anytime.

      Subscribe to Andrew's newsletter for insights & stay on top of the latest news in the Microsoft 365 Space!