repos

blog.bythewood.me-rust

mirror archived upstream

Single-binary self-hosted Markdown blog on Rust axum: no database, live search, Typst PDF export, and strong SEO.

axumblogdockermarkdownminijinjarustself-hostedtypstvite

5.7 KB · 123 lines · markdown Raw History
  1---
  2title: A status line for Claude Code
  3slug: a-status-line-for-claude-code
  4date: 2026-06-13
  5publish_date: 2026-06-13
  6tags: tooling, claude
  7description: A drop-in status line for Claude Code that shows your 5-hour usage quota, context window, working directory with git branch, and the time. One bash file, with a quick breakdown of how it works.
  8cover_image: claude-status-line.webp
  9---
 10
 11Claude Code lets you swap the bar at the bottom of the terminal for any script you want. It pipes a blob of JSON to your script on stdin, and whatever you print becomes the status line. Here is the one I built this week:
 12
 13```
 14usg ▓▓▓▓▓░░░░░ 52% · resets 2h 14m | ctx ▓▓░░░░░░░░ 18% | ~/code/finance  main | 3:45 PM ET
 15```
 16
 17Usage quota, context window, where I am plus the git branch, and the time. One bash file, no API calls, nothing running in the background.
 18
 19## The file
 20
 21Save this as `~/.claude/status-line.sh`:
 22
 23```bash
 24#!/bin/bash
 25# Claude Code status line. Reads the status-line JSON on stdin, prints one line.
 26# Requires: bash, jq, git, date.
 27
 28input=$(cat)
 29
 30cwd=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // empty')
 31[ -z "$cwd" ] && cwd="$PWD"
 32
 33YELLOW=$'\033[93m'; GREEN=$'\033[92m'; BLUE=$'\033[96m'
 34RED=$'\033[91m'; DIM=$'\033[90m'; RESET=$'\033[0m'
 35
 36make_bar() {  # <pct> -> 10-cell ▓/░ bar, one filled cell per 10%
 37    local p="$1" filled empty i out=""
 38    [ "$p" -lt 0 ] && p=0
 39    filled=$(( p / 10 )); [ "$filled" -gt 10 ] && filled=10
 40    empty=$(( 10 - filled ))
 41    i=0; while [ $i -lt $filled ]; do out="${out}▓"; i=$(( i + 1 )); done
 42    i=0; while [ $i -lt $empty ]; do out="${out}░"; i=$(( i + 1 )); done
 43    printf '%s' "$out"
 44}
 45sev_color() {  # green < 50% <= yellow < 80% <= red
 46    local p="$1"
 47    if   [ "$p" -ge 80 ]; then printf '%s' "$RED"
 48    elif [ "$p" -ge 50 ]; then printf '%s' "$YELLOW"
 49    else printf '%s' "$GREEN"; fi
 50}
 51
 52# Context window bar
 53used_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
 54if [ -n "$used_pct" ]; then
 55    pct_int=$(printf "%.0f" "$used_pct")
 56    context_part="${DIM}ctx${RESET} ${YELLOW}$(make_bar "$pct_int") ${pct_int}%${RESET}"
 57else
 58    context_part="${DIM}ctx${RESET} ${YELLOW}░░░░░░░░░░ 0%${RESET}"
 59fi
 60
 61# Working directory (home collapsed to ~) + git branch
 62display_dir=$(echo "$cwd" | sed "s|^$HOME|~|")
 63branch_part=""
 64if git -C "$cwd" -c core.fsmonitor= rev-parse --git-dir >/dev/null 2>&1; then
 65    branch=$(git -C "$cwd" -c core.fsmonitor= symbolic-ref --short HEAD 2>/dev/null \
 66             || git -C "$cwd" -c core.fsmonitor= rev-parse --short HEAD 2>/dev/null)
 67    if [ -n "$branch" ]; then
 68        branch_icon=$(printf '\xee\x82\xa0')  # Nerd Font branch glyph
 69        branch_part=" ${BLUE}${branch_icon} ${branch}${RESET}"
 70    fi
 71fi
 72
 73# 5-hour usage quota (Pro/Max only), severity-colored, with a reset countdown
 74quota_part=""
 75h5=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
 76if [ -n "$h5" ]; then
 77    h5_int=$(printf '%.0f' "$h5")
 78    quota_part="${DIM}usg${RESET} $(sev_color "$h5_int")$(make_bar "$h5_int") ${h5_int}%${RESET}"
 79    reset_at=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty')
 80    if [ -n "$reset_at" ]; then
 81        left=$(( reset_at - $(date +%s) )); [ "$left" -lt 0 ] && left=0
 82        h=$(( left / 3600 )); m=$(( (left % 3600) / 60 ))
 83        if [ "$h" -gt 0 ]; then eta="${h}h ${m}m"; else eta="${m}m"; fi
 84        quota_part="${quota_part} ${DIM}· resets ${eta}${RESET}"
 85    fi
 86fi
 87
 88# Clock, pinned to Eastern regardless of the system clock
 89clock=$(TZ="America/New_York" date '+%-I:%M %p')
 90
 91# Assemble: usg | ctx | cwd+branch | clock. Usage is skipped until its data exists.
 92SEP=" ${DIM}|${RESET} "
 93line="${context_part}${SEP}${GREEN}${display_dir}${RESET}${branch_part}${SEP}${clock} ET"
 94[ -n "$quota_part" ] && line="${quota_part}${SEP}${line}"
 95echo "$line"
 96```
 97
 98## Hook it up
 99
100You need `jq` installed, plus a [Nerd Font](https://www.nerdfonts.com/) in your terminal for the branch glyph. Then add this to `~/.claude/settings.json`:
101
102```json
103"statusLine": {
104  "type": "command",
105  "command": "bash ~/.claude/status-line.sh"
106}
107```
108
109That is the whole integration. Claude Code runs the command, feeds it JSON, and renders what you print.
110
111## How it works
112
113Every render, Claude Code hands your script a JSON object on stdin. The fields here are pulled with `jq` and every one ends in `// empty`, so a missing field just drops its segment instead of printing `null`. That matters because some fields show up late: context usage appears once a turn has run, and the `rate_limits` block only exists on Pro/Max plans after the first API response.
114
115The four segments, left to right:
116
117- **`usg`** is the one I actually wanted. `rate_limits.five_hour.used_percentage` is the same number `/usage` shows, with no extra requests on your side. `resets_at` is a Unix timestamp, so the countdown is just `resets_at - now`. The bar goes green, then yellow past 50%, then red past 80%.
118- **`ctx`** is `context_window.used_percentage`, how full the context window is. Plain yellow, since it is informational.
119- **cwd + branch** uses `git symbolic-ref` for the branch name, falling back to a short commit hash on a detached HEAD. The `-c core.fsmonitor=` keeps this constant polling from fighting a real git operation in the same repo over the fsmonitor lock.
120- **clock** is hard-pinned to `America/New_York`, which tracks the EST/EDT switch on its own. One catch: the status line refreshes on Claude Code events, not a timer, so the clock can sit still while idle. Add `"refreshInterval": 10` to the config if you want it ticking.
121
122The full JSON schema, including fields I skipped like session cost, is in the [statusLine docs](https://code.claude.com/docs/en/statusline).