cd /news/developer-tools/claude-code-statusline-width-aware-t… · home topics developer-tools article
[ARTICLE · art-77935] src=gist.github.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

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

A developer created a custom statusline script for Claude Code that displays model, effort, cost, context bar, duration, rate limits, and git branch info in a width-aware two-line format. The script uses a single jq call and bash builtins to minimize forks, and includes a circle bar for context window usage.

read7 min views24 publishedJul 28, 2026

| #!/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 | | | # state (both tiers, so the fallback chain never adds an empty segment) | |

| context_info="${COLOR}○○○○○○○○○○ ${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}" |
── more in #developer-tools 4 stories · sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/claude-code-statusli…] indexed:0 read:7min 2026-07-28 ·