# Claude Code statusline: width-aware two-line status with model, effort, cost, context bar, duration, rate limits, and repo:branch

> Source: <https://gist.github.com/sfdye/885582e174766bacc82fc5b9c77874b2>
> Published: 2026-07-28 08:03:10+00:00

| #!/bin/bash | |
| shopt -s extglob | |
| # Color configuration | |
| COLOR="\033[96m" # Cyan - easy on eyes at night | |
| MAGENTA="\033[95m" | |
| RESET="\033[0m" | |
| # Read JSON input from stdin (builtin `read`, no `cat` fork) | |
| IFS= read -r -d '' input | |
| # DEBUG: Uncomment the next line to see what JSON is being passed | |
| # echo "$input" > /tmp/statusline-debug.json | |
| # Terminal width: Claude Code (v2.1.153+) sets $COLUMNS on the script's env since | |
| # stdout isn't a tty here, so tput cols/stty can't read it directly. | |
| term_width=${COLUMNS:-80} | |
| # Strip literal "\033[...m" color codes (not real ESC bytes, since they're | |
| # expanded later by `echo -e`) via a bash builtin pattern, to measure the | |
| # visible length of a candidate line without forking printf/sed. | |
| strip_ansi() { | |
| local s="$1" | |
| printf '%s' "${s//\\033\[+([0-9])m/}" | |
| } | |
| # Extract all scalar fields with a single jq call (was 7 separate `echo | jq` | |
| # forks). "|" is safe as a field separator here: unlike tab/@tsv, bash's | |
| # `read` only collapses consecutive IFS-whitespace delimiters, so empty | |
| # fields (e.g. cost/context before the first API call) won't shift columns. | |
| IFS='|' read -r model_name current_dir output_style total_cost context_used max_ctx \ | |
| duration_ms rl_5h_pct rl_7d_pct effort_level <<< "$( | |
| jq -r '[ | |
| .model.display_name, | |
| .workspace.current_dir, | |
| (.output_style.name // "default"), | |
| (.cost.total_cost_usd // ""), | |
| (.context_window.used_percentage // ""), | |
| (.context_window.context_window_size // 200000), | |
| (.cost.total_duration_ms // ""), | |
| (.rate_limits.five_hour.used_percentage // ""), | |
| (.rate_limits.seven_day.used_percentage // ""), | |
| (.effort.level // "") | |
| ] | join("|")' <<< "$input" | |
| )" | |
| # Extract cost information | |
| cost_info="" | |
| if [[ -n "$total_cost" ]]; then | |
| cost_formatted=$(printf "%.4f" "$total_cost") | |
| cost_info="\$${cost_formatted}" | |
| fi | |
| # Context window usage with circle bar (added in Claude Code 2.1.6) | |
| context_info="" | |
| context_info_short="" | |
| if [[ -n "$context_used" ]]; then | |
| pct=$(printf "%.0f" "$context_used") | |
| [[ $pct -gt 100 ]] && pct=100 | |
| # Calculate tokens in k | |
| used_k=$(( max_ctx * pct / 100 / 1000 )) | |
| max_k=$(( max_ctx / 1000 )) | |
| # Build circle bar (10 segments, each worth 10%) | |
| bar="" | |
| filled=$(( pct / 10 )) | |
| # Always blue | |
| BAR_COLOR="\033[94m" | |
| for i in 0 1 2 3 4 5 6 7 8 9; do | |
| if [[ $i -lt $filled ]]; then | |
| bar="${bar}${BAR_COLOR}●${RESET}" | |
| else | |
| bar="${bar}${BAR_COLOR}○${RESET}" | |
| fi | |
| done | |
| context_info="${bar} ${COLOR}${used_k}k/${max_k}k ${pct}%${RESET}" | |
| context_info_short="${COLOR}${pct}%${RESET}" | |
| else | |
| # Loading state (both tiers, so the fallback chain never adds an empty segment) | |
| context_info="${COLOR}○○○○○○○○○○ loading${RESET}" | |
| context_info_short="${COLOR}...${RESET}" | |
| fi | |
| # Get git information (if in a git repo). `rev-parse --abbrev-ref HEAD` alone | |
| # already fails silently (empty output) outside a repo, so no need for a | |
| # separate `rev-parse --git-dir` check first. | |
| git_info="" | |
| branch=$(git -C "$current_dir" rev-parse --abbrev-ref HEAD 2>/dev/null) | |
| if [[ -n "$branch" ]]; then | |
| # Get git status with --no-optional-locks to avoid lock issues | |
| git_status=$(git -C "$current_dir" --no-optional-locks status --porcelain 2>/dev/null) | |
| if [[ -n "$git_status" ]]; then | |
| # Count changes in a single awk pass instead of 3 separate greps | |
| read -r modified added untracked <<< "$(awk ' | |
| /^ M/{m++} /^A/{a++} /^\?\?/{u++} | |
| END{print m+0, a+0, u+0} | |
| ' <<< "$git_status")" | |
| status_indicator="" | |
| [[ $modified -gt 0 ]] && status_indicator="${status_indicator}~${modified}" | |
| [[ $added -gt 0 ]] && status_indicator="${status_indicator}+${added}" | |
| [[ $untracked -gt 0 ]] && status_indicator="${status_indicator}?${untracked}" | |
| git_info="${branch}${status_indicator}" | |
| else | |
| git_info="${branch}" | |
| fi | |
| fi | |
| # repo:branch (e.g. "zra-ingestor:lwan/foo") — the org/parent dir segment | |
| # rarely adds information, so just pair the repo leaf name with its branch. | |
| dir_basename="${current_dir##*/}" | |
| if [[ -n "$git_info" ]]; then | |
| repo_info="${dir_basename}:${git_info}" | |
| else | |
| repo_info="$dir_basename" | |
| fi | |
| # Effort level (only shown if set), magenta to stand out from the rest of the line | |
| effort_info="" | |
| if [[ -n "$effort_level" ]]; then | |
| effort_info="${MAGENTA}${effort_level}${RESET}" | |
| fi | |
| # Output style suffix (only shown if non-default) | |
| style_info="" | |
| if [[ "$output_style" != "default" ]]; then | |
| style_info="style:${output_style}" | |
| fi | |
| # Session duration, e.g. "1h23m" / "9m" | |
| duration_info="" | |
| if [[ -n "$duration_ms" ]]; then | |
| total_sec=$(( duration_ms / 1000 )) | |
| dur_h=$(( total_sec / 3600 )) | |
| dur_m=$(( (total_sec % 3600) / 60 )) | |
| if (( dur_h > 0 )); then | |
| duration_info="${dur_h}h${dur_m}m" | |
| else | |
| duration_info="${dur_m}m" | |
| fi | |
| fi | |
| # 5-hour / 7-day rate limit usage, e.g. "5h:30% 7d:12%" | |
| rate_limit_info="" | |
| rate_limit_info_short="" | |
| if [[ -n "$rl_5h_pct" || -n "$rl_7d_pct" ]]; then | |
| rl_5h_disp=$(printf "%.0f" "${rl_5h_pct:-0}") | |
| rl_7d_disp=$(printf "%.0f" "${rl_7d_pct:-0}") | |
| rate_limit_info="5h:${rl_5h_disp}% 7d:${rl_7d_disp}%" | |
| rate_limit_info_short="${rl_5h_disp}/${rl_7d_disp}%" | |
| fi | |
| # Full current dir, with $HOME collapsed to "~" (as in a typical shell prompt) | |
| full_dir="${current_dir/#$HOME/~}" | |
| # Build a line by adding segments in priority order (highest first) into | |
| # $status_line, up to $term_width. Each segment can pass multiple | |
| # decreasing-detail variants; add_segment tries them in order and keeps the | |
| # first that fits, skipping empty ones. | |
| SEP=" | " | |
| add_segment() { | |
| local variant candidate visible | |
| for variant in "$@"; do | |
| [[ -z "$variant" ]] && continue | |
| candidate="${status_line:+${status_line}${SEP}}${variant}" | |
| visible=$(strip_ansi "$candidate") | |
| if (( ${#visible} <= term_width )); then | |
| status_line="$candidate" | |
| return 0 | |
| fi | |
| done | |
| return 1 | |
| } | |
| # Line 1 priority: model > effort > cost > context > duration > rate limits | |
| # model+effort are joined with a plain space (no separator) so effort reads | |
| # as a modifier on the model name rather than its own segment. | |
| status_line="" | |
| add_segment "$model_name" | |
| if [[ -n "$effort_info" ]]; then | |
| status_line="${status_line} ${effort_info}" | |
| fi | |
| add_segment "$cost_info" | |
| add_segment "$context_info" "$context_info_short" | |
| add_segment "$duration_info" | |
| add_segment "$rate_limit_info" "$rate_limit_info_short" | |
| line1="$status_line" | |
| # Line 2 priority: repo:branch > style > full path | |
| status_line="" | |
| add_segment "$repo_info" "📁 $dir_basename" | |
| add_segment "$style_info" | |
| add_segment "$full_dir" | |
| line2="$status_line" | |
| echo -e "${COLOR}${line1}${RESET}" | |
| echo -e "${COLOR}${line2}${RESET}" |
