Skip to content

Instantly share code, notes, and snippets.

@joematthews
Last active March 24, 2026 16:31
Show Gist options
  • Select an option

  • Save joematthews/a6a10a3a56a7c2387cbac546fe491719 to your computer and use it in GitHub Desktop.

Select an option

Save joematthews/a6a10a3a56a7c2387cbac546fe491719 to your computer and use it in GitHub Desktop.
Claude Code Custom Status Prompt

Claude Code Custom Status Line

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.

What it looks like

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

What each part means

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%+)

Requirements

  • jq - JSON processor. Install with: brew install jq (macOS) or apt install jq (Linux)
  • git - For branch/status info (optional, works without it)

Setup

Quick install (recommended)

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.

Manual install

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.sh

2. Add to ~/.claude/settings.json:

{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline-command.sh"
  }
}

The Script

#!/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"

Available JSON properties

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
    }
  }
}

Notes

  • macOS only: stat -f %m is macOS-specific. On Linux, replace with stat -c %Y.
  • exceeds_200k_tokens is a legacy flag from when the context window was 200K. Not useful with 1M context — use used_percentage instead.
  • Rate limits are specific to the Max plan. Other plans may have different or no rate limit fields.
  • Update check: claude update both checks and installs updates. The cache lives at ~/.claude/.update-check-cache and refreshes hourly.
  • cost fields are available but not shown by default. Useful for API/pay-per-use plans, less so for Max.
  • output_style.name is "default" or "fast" — toggle with /fast in Claude Code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment