Custom Claude Code Status Line

Custom Claude Code Status Line

Claude Code has a fully customizable status line. You point it at a shell script, it pipes in session data as JSON, and your script renders whatever you want. I built a powerline-style status bar that gives me everything I need at a glance.

Claude Code powerline status line

Left to right:

  • Model (Opus 4.6) — active Claude model
  • Folder (git-manager/dev) — current repo as parent/folder
  • Git branch (dev) — only shows when not on main
  • Kubernetes context (k8s-blue-cc) — active cluster
  • Context window (86%) — conversation fullness, scaled so 100% = compaction threshold
  • 5h usage (2%, resets in 4h6m) — rolling 5-hour API quota
  • 7d usage (53%, resets Thu 11:00) — rolling 7-day API quota

Each meter is color-coded: green under 50%, yellow 50-79%, red 80%+.

How It Works

Claude Code calls your script after each assistant message, piping a JSON blob to stdin with session metadata (model, workspace, context_window with token breakdown, cost, session_id, vim.mode, etc.). Your script reads it and prints ANSI-colored output to stdout.

The script has four parts: parse JSON for model/folder/branch, fetch API usage from Anthropic’s OAuth endpoint (cached 60s to avoid hammering the API), render color-coded progress bars, and assemble powerline segments with ANSI escape codes.

The API usage part reads the OAuth token from macOS Keychain via security find-generic-password. On Linux, you’d need a different approach to read the token.

Installation

Requirements: Nerd Font (for powerline separators and icons), jq, kubectl (optional — remove the k8s segment if you don’t need it).

  1. Save the full script below to ~/.claude/statusline.sh and make it executable:
chmod +x ~/.claude/statusline.sh
  1. Add to ~/.claude/settings.json:
{
  "statusLine": {
    "type": "command",
    "command": "bash ~/.claude/statusline.sh"
  }
}
  1. Restart Claude Code. The status line appears above the input prompt.

Adapting It

Each segment is independent — just ask Claude Code to adapt the script to your needs.

Full Script

statusline.sh
#!/bin/bash

# Modern Powerline-style statusline for Claude Code
# Requires: Nerd Font (for icons and separators)

# Read JSON input from stdin
input=$(cat)

# ═══════════════════════════════════════════════════════════════════════════════
# POWERLINE CHARACTERS & COLORS
# ═══════════════════════════════════════════════════════════════════════════════

# Powerline separators (Nerd Font)
SEP=""      # U+E0B0 - right arrow (filled)
SEPR=""     # U+E0B2 - left arrow (filled)

# Color definitions (256-color mode)
# Format: \033[38;5;XXXm = foreground, \033[48;5;XXXm = background
RST='\033[0m'

# Segment colors (bg, fg pairs)
# Using a cohesive dark palette
BG_MODEL="\033[48;5;24m"     # Deep blue
FG_MODEL="\033[38;5;255m"    # White text
FG_MODEL_SEP="\033[38;5;24m" # For separator after

BG_FOLDER="\033[48;5;30m"    # Teal
FG_FOLDER="\033[38;5;255m"   # White
FG_FOLDER_SEP="\033[38;5;30m"

BG_BRANCH="\033[48;5;132m"    # Mauve (git branch segment)
FG_BRANCH="\033[38;5;255m"   # White
FG_BRANCH_SEP="\033[38;5;132m"

BG_K8S="\033[48;5;91m"       # Purple
FG_K8S="\033[38;5;255m"      # White
FG_K8S_SEP="\033[38;5;91m"

BG_CTX="\033[48;5;237m"      # Dark grey
FG_CTX="\033[38;5;250m"      # Light grey
FG_CTX_SEP="\033[38;5;237m"

BG_5H="\033[48;5;236m"       # Darker grey
FG_5H="\033[38;5;250m"
FG_5H_SEP="\033[38;5;236m"

BG_7D="\033[48;5;235m"       # Darkest grey
FG_7D="\033[38;5;250m"
FG_7D_SEP="\033[38;5;235m"

# Status colors for usage levels
FG_OK="\033[38;5;77m"        # Green
FG_WARN="\033[38;5;220m"     # Yellow
FG_CRIT="\033[38;5;203m"     # Red

# Nerd Font icons
ICON_MODEL="󰚩"    # nf-md-robot
ICON_FOLDER="󰉋"   # nf-md-folder
ICON_GIT="󰊢"      # nf-md-git
ICON_BRANCH=""   # nf-oct-git_branch
ICON_K8S="󱃾"      # nf-md-kubernetes
ICON_CTX=""     # nf-fa-tachometer_alt / gauge
ICON_5H=""      # nf-fa-clock
ICON_7D=""      # nf-fa-calendar_week

# ═══════════════════════════════════════════════════════════════════════════════
# DATA EXTRACTION
# ═══════════════════════════════════════════════════════════════════════════════

# Extract basic info
model=$(echo "$input" | jq -r '.model.display_name')
cwd=$(echo "$input" | jq -r '.workspace.current_dir')

# Get folder and branch (separate segments)
folder="$(basename "$(dirname "$cwd")")/$(basename "$cwd")"
folder_icon="${ICON_FOLDER}"
show_branch=""
branch_name=""

if git -C "$cwd" rev-parse --is-inside-work-tree &>/dev/null; then
    branch_name=$(git -C "$cwd" branch --show-current 2>/dev/null)
    if [[ "$branch_name" != "main" && -n "$branch_name" ]]; then
        show_branch="yes"
    fi
fi

# Context window usage
context_size=$(echo "$input" | jq -r '.context_window.context_window_size // 0')
context_usage=$(echo "$input" | jq -r '.context_window.current_usage // null')
if [[ "$context_usage" != "null" && "$context_size" -gt 0 ]]; then
    context_tokens=$(echo "$input" | jq -r '.context_window.current_usage | (.input_tokens // 0) + (.cache_creation_input_tokens // 0) + (.cache_read_input_tokens // 0)')
    context_pct_raw=$((context_tokens * 100 / context_size))
    # Scale so 82% (conservative threshold) displays as 100%
    # This gives more headroom before compaction at 78%
    context_pct=$((context_pct_raw * 100 / 82))
    [[ $context_pct -gt 100 ]] && context_pct=100
else
    context_pct=0
fi

# Kubernetes context
k8s_context=$(kubectl config current-context 2>/dev/null || echo "none")

# ═══════════════════════════════════════════════════════════════════════════════
# API USAGE (5h/7d from Anthropic API)
# ═══════════════════════════════════════════════════════════════════════════════

USAGE_CACHE="$HOME/.cache/cc-usage.txt"
USAGE_LOCK="$HOME/.cache/cc-usage.lock"
USAGE_TTL="${CC_CACHE_TTL:-60}"
mkdir -p "$HOME/.cache"

get_api_usage() {
    if [[ -f "$USAGE_CACHE" ]]; then
        age=$(($(date +%s) - $(stat -f '%m' "$USAGE_CACHE" 2>/dev/null || echo 0)))
        if [[ $age -lt $USAGE_TTL ]]; then
            cat "$USAGE_CACHE"
            return
        fi
    fi

    if [[ -f "$USAGE_LOCK" ]]; then
        age=$(($(date +%s) - $(stat -f '%m' "$USAGE_LOCK" 2>/dev/null || echo 0)))
        if [[ $age -lt 30 ]]; then
            [[ -f "$USAGE_CACHE" ]] && cat "$USAGE_CACHE"
            return
        fi
    fi

    touch "$USAGE_LOCK"

    creds=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null)
    if [[ -z "$creds" ]]; then
        echo "?|?|?|?"
        return
    fi

    token=$(echo "$creds" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null)
    if [[ -z "$token" ]]; then
        echo "?|?|?|?"
        return
    fi

    resp=$(curl -s --max-time 5 \
        "https://api.anthropic.com/api/oauth/usage" \
        -H "Authorization: Bearer $token" \
        -H "anthropic-beta: oauth-2025-04-20" 2>/dev/null) || true

    if [[ -z "$resp" ]]; then
        [[ -f "$USAGE_CACHE" ]] && cat "$USAGE_CACHE" || echo "?|?|?|?"
        return
    fi

    session=$(echo "$resp" | jq -r '.five_hour.utilization // empty' 2>/dev/null)
    weekly=$(echo "$resp" | jq -r '.seven_day.utilization // empty' 2>/dev/null)

    if [[ -z "$session" || -z "$weekly" ]]; then
        [[ -f "$USAGE_CACHE" ]] && cat "$USAGE_CACHE" || echo "?|?|?|?"
        return
    fi

    session_int=$(printf "%.0f" "$session")
    weekly_int=$(printf "%.0f" "$weekly")

    session_reset=$(echo "$resp" | jq -r '.five_hour.resets_at // empty' 2>/dev/null)
    if [[ -n "$session_reset" ]]; then
        utc_epoch_5h=$(date -j -u -f "%Y-%m-%dT%H:%M:%S" "${session_reset%%.*}" "+%s" 2>/dev/null)
        if [[ -n "$utc_epoch_5h" ]]; then
            now_epoch=$(date +%s)
            diff_sec=$((utc_epoch_5h - now_epoch))
            if [[ $diff_sec -gt 0 ]]; then
                diff_hr=$((diff_sec / 3600))
                diff_min=$(((diff_sec % 3600) / 60))
                session_reset_fmt="${diff_hr}h${diff_min}m"
            else
                session_reset_fmt="now"
            fi
        else
            session_reset_fmt="?"
        fi
    else
        session_reset_fmt="?"
    fi

    weekly_reset=$(echo "$resp" | jq -r '.seven_day.resets_at // empty' 2>/dev/null)
    if [[ -n "$weekly_reset" ]]; then
        utc_epoch=$(date -j -u -f "%Y-%m-%dT%H:%M:%S" "${weekly_reset%%.*}" "+%s" 2>/dev/null)
        if [[ -n "$utc_epoch" ]]; then
            weekly_reset_fmt=$(date -j -f "%s" "$utc_epoch" "+%a %H:%M" 2>/dev/null || echo "?")
        else
            weekly_reset_fmt="?"
        fi
    else
        weekly_reset_fmt="?"
    fi

    echo "${session_int}|${weekly_int}|${session_reset_fmt}|${weekly_reset_fmt}" | tee "$USAGE_CACHE"
}

api_usage_raw=$(get_api_usage)
usage_5h=$(echo "$api_usage_raw" | cut -d'|' -f1)
usage_7d=$(echo "$api_usage_raw" | cut -d'|' -f2)
reset_5h=$(echo "$api_usage_raw" | cut -d'|' -f3)
reset_7d=$(echo "$api_usage_raw" | cut -d'|' -f4)

# ═══════════════════════════════════════════════════════════════════════════════
# PROGRESS BAR (compact dot style)
# ═══════════════════════════════════════════════════════════════════════════════

progress_bar() {
    local percent=$1
    local width=5

    if ! [[ "$percent" =~ ^[0-9]+$ ]]; then
        printf "○○○○○"
        return
    fi

    local filled=$((percent * width / 100))
    [[ $filled -gt $width ]] && filled=$width
    [[ $filled -lt 0 ]] && filled=0
    local empty=$((width - filled))

    local bar=""
    for ((i=0; i<filled; i++)); do bar+="●"; done
    for ((i=0; i<empty; i++)); do bar+="○"; done
    printf "%s" "$bar"
}

# Get color based on percentage
get_usage_color() {
    local pct=$1
    if ! [[ "$pct" =~ ^[0-9]+$ ]]; then
        echo "$FG_CTX"
        return
    fi
    if [[ $pct -ge 80 ]]; then
        echo "$FG_CRIT"
    elif [[ $pct -ge 50 ]]; then
        echo "$FG_WARN"
    else
        echo "$FG_OK"
    fi
}

# Generate bars with colors
bar_ctx=$(progress_bar "$context_pct")
bar_5h=$(progress_bar "$usage_5h")
bar_7d=$(progress_bar "$usage_7d")

color_ctx=$(get_usage_color "$context_pct")
color_5h=$(get_usage_color "$usage_5h")
color_7d=$(get_usage_color "$usage_7d")

# ═══════════════════════════════════════════════════════════════════════════════
# BUILD POWERLINE OUTPUT
# ═══════════════════════════════════════════════════════════════════════════════

# Segment 1: Model
printf "${BG_MODEL}${FG_MODEL} ${ICON_MODEL} %s " "$model"
printf "${BG_FOLDER}${FG_MODEL_SEP}${SEP}"

# Segment 2: Folder
printf "${BG_FOLDER}${FG_FOLDER} ${folder_icon} %s " "$folder"

# Segment 3: Branch (only if not on main)
if [[ -n "$show_branch" ]]; then
    printf "${BG_BRANCH}${FG_FOLDER_SEP}${SEP}"
    printf "${BG_BRANCH}${FG_BRANCH} ${ICON_GIT}${ICON_BRANCH} %s " "$branch_name"
    printf "${BG_K8S}${FG_BRANCH_SEP}${SEP}"
else
    printf "${BG_K8S}${FG_FOLDER_SEP}${SEP}"
fi

# Segment 4: Kubernetes
printf "${BG_K8S}${FG_K8S} ${ICON_K8S} %s " "$k8s_context"
printf "${BG_CTX}${FG_K8S_SEP}${SEP}"

# Segment 4: Context usage
printf "${BG_CTX}${FG_CTX} ${ICON_CTX} ${color_ctx}%s${FG_CTX} %s%% " "$bar_ctx" "$context_pct"
printf "${BG_5H}${FG_CTX_SEP}${SEP}"

# Segment 5: 5h usage
printf "${BG_5H}${FG_5H} ${ICON_5H} ${color_5h}%s${FG_5H} %s%% %s " "$bar_5h" "$usage_5h" "$reset_5h"
printf "${BG_7D}${FG_5H_SEP}${SEP}"

# Segment 6: 7d usage
printf "${BG_7D}${FG_7D} ${ICON_7D} ${color_7d}%s${FG_7D} %s%% %s " "$bar_7d" "$usage_7d" "$reset_7d"
printf "${RST}${FG_7D_SEP}${SEP}${RST}"
Last updated on