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