A bash script that adds a rich status bar to Claude Code CLI, showing model info, context usage, rate limits with countdown timers, update alerts, and git status at a glance.
Single repo:
Opus 4.6 (1M context) 45K (12%) 5h: 21% ↻2h13m 7d: 11% ↻4d3h
my-project → main !3 ?2 ↑1
Monorepo (multiple git repos in subdirectories):
Opus 4.6 (1M context) 45K (12%) 5h: 21% ↻2h13m 7d: 11% ↻4d3h
workspace
├── frontend → main !2
└── backend → develop ↑3 ↓1
With update available:
Opus 4.6 (1M context) 45K (12%) 5h: 21% ↻2h13m 7d: 11% ↻4d3h ⬆ update
| Symbol | Color | Meaning |
|---|---|---|
45K (12%) |
Tokens used and percentage of context window consumed | |
5h: 21% ↻2h13m |
green/yellow/red | 5-hour rate limit usage, resets in 2 hours 13 minutes |
7d: 11% ↻4d3h |
green/yellow/red | 7-day rate limit usage, resets in 4 days 3 hours |
⬆ update |
yellow | A newer version of Claude Code is available (checked hourly) |
→ main |
Current git branch | |
3 |
green | 3 staged files (ready to commit) |
!2 |
yellow | 2 files with unstaged changes |
?1 |
red | 1 untracked (new) file |
↑1 |
cyan | 1 commit ahead of remote (needs push) |
↓2 |
magenta | 2 commits behind remote (needs pull) |
Rate limit color thresholds: green (< 50%), yellow (50-79%), red (80%+)
- jq - JSON processor. Install with:
brew install jq(macOS) orapt install jq(Linux) - git - For branch/status info (optional, works without it)
If you're already in Claude Code, paste this:
Read and implement https://gist.github.com/joematthews/a6a10a3a56a7c2387cbac546fe491719
Claude Code will save the script, make it executable, and update your settings.
1. Save the script:
curl -o ~/.claude/statusline-command.sh \
https://gist.githubusercontent.com/joematthews/a6a10a3a56a7c2387cbac546fe491719/raw/statusline-command.sh
chmod +x ~/.claude/statusline-command.sh2. Add to ~/.claude/settings.json:
{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline-command.sh"
}
}#!/bin/bash
# =============================================================================
# Claude Code Status Line Script
# =============================================================================
#
# Displays a real-time status bar inside Claude Code with:
# Line 1: Model info, context usage, rate limits, and update alerts
# Line 2: Current directory with git branch and file change indicators
#
# Line 1 example:
# Opus 4.6 (1M context) 45K (12%) 5h: 21% ↻2h13m 7d: 11% ↻4d3h
#
# Line 2 examples (single repo vs monorepo):
# my-project → main !3 ?2 ↑1
# workspace
# ├── frontend → main !2
# └── backend → develop ↑3 ↓1
#
# SETUP:
# 1. Save this file to ~/.claude/statusline-command.sh
# 2. Make it executable: chmod +x ~/.claude/statusline-command.sh
# 3. Add to ~/.claude/settings.json:
# {
# "statusLine": {
# "type": "command",
# "command": "~/.claude/statusline-command.sh"
# }
# }
#
# REQUIRES: jq (JSON processor) - install with: brew install jq
# =============================================================================
# --- Verify jq is installed (needed to read Claude Code's JSON input) ---
if ! command -v jq &> /dev/null; then
echo "Error: jq is required (brew install jq)"
exit 1
fi
# Claude Code pipes a JSON object to this script via stdin with session data.
# We capture it once and extract fields from it throughout the script.
input=$(cat)
# --- ANSI color codes for terminal styling ---
BOLD='\033[1m' # Bold text (model name, labels, arrow separator)
CYAN='\033[1;36m' # Directory names
GREEN='\033[32m' # Staged files (ready to commit)
YELLOW='\033[1;33m' # Unstaged changes / caution-level rate limits
RED='\033[1;31m' # Untracked files / danger-level rate limits
MAGENTA='\033[1;35m' # Commits behind remote (needs pull)
RESET='\033[0m' # Resets all styling back to normal
# =============================================================================
# LINE 1: Model info, context, rate limits
# =============================================================================
# --- Extract all needed values from JSON in a single jq call for speed ---
# Instead of calling jq many times (slow), we pull everything at once.
# The @sh filter safely quotes values for shell eval (no injection risk).
eval "$(echo "$input" | jq -r '
"model=" + (.model.display_name // "Claude" | @sh),
"used_pct=" + (.context_window.used_percentage // 0 | tostring),
"input_tokens=" + (.context_window.current_usage.input_tokens // 0 | tostring),
"cache_create=" + (.context_window.current_usage.cache_creation_input_tokens // 0 | tostring),
"cache_read=" + (.context_window.current_usage.cache_read_input_tokens // 0 | tostring),
"rate_5h=" + (.rate_limits.five_hour.used_percentage // 0 | ceil | tostring),
"rate_7d=" + (.rate_limits.seven_day.used_percentage // 0 | ceil | tostring),
"reset_5h=" + (.rate_limits.five_hour.resets_at // 0 | tostring),
"reset_7d=" + (.rate_limits.seven_day.resets_at // 0 | tostring)
')"
# --- Context window usage (how much of the conversation memory is used) ---
# Tokens are the units of text the AI processes. More tokens = more context used.
# We add up all token types to get total usage.
context_info=""
if [ "$used_pct" -gt 0 ]; then
current_tokens=$((input_tokens + cache_create + cache_read))
# Show as "45K" instead of "45000" for readability
if [ "$current_tokens" -gt 1000 ]; then
token_display="$((current_tokens / 1000))K"
else
token_display="$current_tokens"
fi
context_info=" ${token_display} (${used_pct}%)"
fi
# --- Rate limit colors (green = fine, yellow = caution, red = near limit) ---
rate_color() {
local pct=$1
if [ "$pct" -ge 80 ]; then echo -ne "$RED"
elif [ "$pct" -ge 50 ]; then echo -ne "$YELLOW"
else echo -ne "$GREEN"
fi
}
# --- Countdown timer (converts a future unix timestamp to "2h13m" format) ---
format_countdown() {
local resets_at=$1
local now
now=$(date +%s)
local diff=$((resets_at - now))
# Already reset
if [ "$diff" -le 0 ]; then
echo "now"
return
fi
local hours=$((diff / 3600))
local mins=$(( (diff % 3600) / 60 ))
if [ "$hours" -gt 24 ]; then
# Show days + hours for longer countdowns
local days=$((hours / 24))
hours=$((hours % 24))
echo "${days}d${hours}h"
elif [ "$hours" -gt 0 ]; then
echo "${hours}h${mins}m"
else
echo "${mins}m"
fi
}
countdown_5h=$(format_countdown "$reset_5h")
countdown_7d=$(format_countdown "$reset_7d")
# --- Update check (cached for 1 hour, runs in background) ---
# IMPORTANT: "claude update" both checks AND installs updates when available.
# We cache the result so this only runs once per hour, not on every render.
# NOTE: stat -f %m is macOS-specific. On Linux, use: stat -c %Y
update_cache="$HOME/.claude/.update-check-cache"
update_indicator=""
if [ ! -f "$update_cache" ] || [ $(($(date +%s) - $(stat -f %m "$update_cache" 2>/dev/null || echo 0))) -gt 3600 ]; then
# Touch first so other renders don't also trigger a check while this one runs
touch "$update_cache"
# Run in background (&) so it doesn't block the status line from rendering
(claude update 2>&1 > "$update_cache" &)
fi
# Only show the update indicator when "claude update" explicitly says one is available
if [ -f "$update_cache" ] && grep -q "Update available" "$update_cache" 2>/dev/null; then
update_indicator=" ${YELLOW}⬆ update${RESET}"
fi
# --- Print line 1 ---
# Example: Opus 4.6 (1M context) 45K (12%) 5h: 21% ↻2h13m 7d: 11% ↻4d3h
printf "${BOLD}%s${RESET}%s " "$model" "$context_info"
printf "${BOLD}5h:${RESET} $(rate_color "$rate_5h")${rate_5h}%% ↻${countdown_5h}${RESET} "
printf "${BOLD}7d:${RESET} $(rate_color "$rate_7d")${rate_7d}%% ↻${countdown_7d}${RESET}"
printf "%b\n" "$update_indicator"
# =============================================================================
# LINE 2+: Directory and git status
# =============================================================================
# --- Git info for a single repository ---
# Shows: branch name, staged files, unstaged changes, untracked files, ahead/behind remote
# Example output: main 1 !3 ?2 ↑1
get_repo_git_info() {
local repo_path=$1
# Check if this directory is actually a git repo
if ! git -C "$repo_path" rev-parse --git-dir > /dev/null 2>&1; then
return 1
fi
# Get current branch name (or short commit hash if in detached HEAD state)
# --no-optional-locks prevents git from waiting on lock files
local branch
branch=$(git -C "$repo_path" --no-optional-locks branch --show-current 2>/dev/null)
if [ -z "$branch" ]; then
branch=$(git -C "$repo_path" --no-optional-locks rev-parse --short HEAD 2>/dev/null)
fi
local status_parts=()
# Count staged files (files added to the next commit with "git add") - green
local staged
staged=$(git -C "$repo_path" --no-optional-locks diff --cached --numstat 2>/dev/null | wc -l | tr -d ' ')
[ "$staged" != "0" ] && status_parts+=("${GREEN}${staged}${RESET}")
# Count unstaged changes (modified files not yet "git add"ed) - yellow with !
local unstaged
unstaged=$(git -C "$repo_path" --no-optional-locks diff --numstat 2>/dev/null | wc -l | tr -d ' ')
[ "$unstaged" != "0" ] && status_parts+=("${YELLOW}!${unstaged}${RESET}")
# Count untracked files (brand new files git doesn't know about) - red with ?
local untracked
untracked=$(git -C "$repo_path" --no-optional-locks ls-files --others --exclude-standard 2>/dev/null | wc -l | tr -d ' ')
[ "$untracked" != "0" ] && status_parts+=("${RED}?${untracked}${RESET}")
# Check if local branch is ahead of or behind the remote branch
# ↑ = commits to push (cyan), ↓ = commits to pull (magenta)
local upstream
upstream=$(git -C "$repo_path" --no-optional-locks rev-parse --abbrev-ref @{upstream} 2>/dev/null)
if [ -n "$upstream" ]; then
local ahead behind
ahead=$(git -C "$repo_path" --no-optional-locks rev-list --count @{upstream}..HEAD 2>/dev/null || echo "0")
behind=$(git -C "$repo_path" --no-optional-locks rev-list --count HEAD..@{upstream} 2>/dev/null || echo "0")
[ "$ahead" != "0" ] && status_parts+=("${CYAN}↑${ahead}${RESET}")
[ "$behind" != "0" ] && status_parts+=("${MAGENTA}↓${behind}${RESET}")
fi
# Assemble the output (echo -e renders the embedded ANSI color codes)
if [ ${#status_parts[@]} -gt 0 ]; then
echo -e "$branch $(IFS=' '; echo "${status_parts[*]}")"
else
echo -e "$branch"
fi
}
# --- Determine if we're in a git repo or a parent of multiple repos ---
cwd=$(basename "$(pwd)")
is_git_repo=false
if git rev-parse --git-dir > /dev/null 2>&1; then
# Current directory is a git repo - show its status directly
is_git_repo=true
repo_status=$(get_repo_git_info ".")
else
repo_status="no git"
fi
# --- Print line 2+ ---
if [ "$is_git_repo" = false ]; then
# Not a git repo - check if subdirectories are repos (monorepo layout)
git_repos=()
for dir in */; do
if [ -d "$dir" ] && git -C "$dir" rev-parse --git-dir > /dev/null 2>&1; then
git_repos+=("$dir")
fi
done
if [ ${#git_repos[@]} -eq 0 ]; then
# No git repos found anywhere - just show directory name
printf "${CYAN}%s${RESET} ${BOLD}→${RESET} %b" "$cwd" "$repo_status"
else
# Monorepo: show parent directory with tree branches for each sub-repo
printf "${CYAN}%s${RESET}" "$cwd"
total=${#git_repos[@]}
for i in "${!git_repos[@]}"; do
dir="${git_repos[$i]}"
dir_name=$(basename "$dir")
dir_status=$(get_repo_git_info "$dir")
# └── for last item, ├── for all others (tree-style formatting)
if [ $((i + 1)) -eq $total ]; then
printf "\n└── ${CYAN}%s${RESET} ${BOLD}→${RESET} %b" "$dir_name" "$dir_status"
else
printf "\n├── ${CYAN}%s${RESET} ${BOLD}→${RESET} %b" "$dir_name" "$dir_status"
fi
done
fi
else
# Single git repo - show directory and status on one line
printf "${CYAN}%s${RESET} ${BOLD}→${RESET} %b" "$cwd" "$repo_status"
fi
printf "\n"Claude Code pipes this JSON to the statusline script via stdin. Here's the full schema (as of v2.1.81):
{
"session_id": "uuid",
"transcript_path": "/path/to/session.jsonl",
"cwd": "/current/working/directory",
"model": {
"id": "claude-opus-4-6[1m]",
"display_name": "Opus 4.6 (1M context)"
},
"workspace": {
"current_dir": "/path",
"project_dir": "/path",
"added_dirs": []
},
"version": "2.1.81",
"output_style": {
"name": "default"
},
"cost": {
"total_cost_usd": 2.03,
"total_duration_ms": 5061510,
"total_api_duration_ms": 583675,
"total_lines_added": 51,
"total_lines_removed": 7
},
"context_window": {
"total_input_tokens": 14180,
"total_output_tokens": 15782,
"context_window_size": 1000000,
"current_usage": {
"input_tokens": 1,
"output_tokens": 32,
"cache_creation_input_tokens": 182,
"cache_read_input_tokens": 19051
},
"used_percentage": 2,
"remaining_percentage": 98
},
"exceeds_200k_tokens": false,
"rate_limits": {
"five_hour": {
"used_percentage": 21,
"resets_at": 1774375200
},
"seven_day": {
"used_percentage": 11,
"resets_at": 1774638000
}
}
}- macOS only:
stat -f %mis macOS-specific. On Linux, replace withstat -c %Y. exceeds_200k_tokensis a legacy flag from when the context window was 200K. Not useful with 1M context — useused_percentageinstead.- Rate limits are specific to the Max plan. Other plans may have different or no rate limit fields.
- Update check:
claude updateboth checks and installs updates. The cache lives at~/.claude/.update-check-cacheand refreshes hourly. costfields are available but not shown by default. Useful for API/pay-per-use plans, less so for Max.output_style.nameis"default"or"fast"— toggle with/fastin Claude Code.