Skip to content

Instantly share code, notes, and snippets.

@sjungling
Last active September 4, 2025 22:46
Show Gist options
  • Save sjungling/b00d4b81c0ab6c7639c79eee9d95abdc to your computer and use it in GitHub Desktop.
Save sjungling/b00d4b81c0ab6c7639c79eee9d95abdc to your computer and use it in GitHub Desktop.
Claude Code Status Line - A Go program that displays workspace status including directory, git branch, model info, and usage metrics for Claude Code CLI

Claude Code Status Line

A bash script that generates a status line for the Claude Code CLI, displaying workspace information including:

  • Current directory name
  • Git branch status (clean/dirty)
  • Active Claude model
  • Output style (if not default)
  • Usage metrics with intelligent indicators (cost, burn rate, time remaining)

Prerequisites

  • Bash shell
  • jq (for JSON parsing)
  • bc (for floating-point calculations)
  • Git (for git status information)
  • ccusage command (for Claude Code usage metrics)

Usage

The script is ready to use without compilation:

# Run directly
./statusline-command.sh < input.json

# With mode flags
./statusline-command.sh --mode=pro < input.json
./statusline-command.sh --mode=api < input.json

Testing

Using Makefile

# Run comprehensive tests with timing measurements
make test

This will test different modes and input styles with performance timing.

Manual Testing

# Test with Pro mode (default)
echo '{"workspace":{"current_dir":"/path"},"model":{"display_name":"Claude 3.5 Sonnet"},"output_style":{"name":"default"}}' | ./statusline-command.sh

# Test with API mode
echo '{"workspace":{"current_dir":"/path"},"model":{"display_name":"Claude 3.5 Sonnet"},"output_style":{"name":"default"}}' | ./statusline-command.sh --mode=api

Installation

# Install to ~/.claude directory
make install

This copies the script as statusline to ~/.claude/ and makes it executable.

Modes

The script supports two modes:

  • Pro mode (--mode=pro, default): Shows block cost, burn rate with quota-aware indicators, and time remaining
  • API mode (--mode=api): Shows session cost and estimated request count

Burn Rate Indicators

The Pro mode uses intelligent emoji indicators based on usage intensity:

  • 🟢 Low burn (0-25% of sustainable rate)
  • 🟡 Moderate burn (25-50%)
  • 🟠 High burn (50-75%)
  • 🔥 Critical burn (75-100%)
  • 🚨 Over-quota burn (>100%)

Input Format

The script reads JSON input from stdin containing workspace information and outputs a formatted status line with ANSI color codes.

{
  "workspace": {
    "current_dir": "/path/to/workspace"
  },
  "cwd": "/fallback/path",
  "model": {
    "display_name": "Claude 3.5 Sonnet",
    "id": "claude-3-5-sonnet"
  },
  "output_style": {
    "name": "custom-style"
  }
}

Example Output

Pro mode:

my-project on main via Claude 3.5 Sonnet in custom-style │ 5¢ │ 🟡12¢/hr │ 25m left

API mode:

my-project on main via Claude 3.5 Sonnet │ Session: 15¢ │ ~7 requests

Features

  • Directory Display: Shows current workspace directory name
  • Git Integration: Displays current branch with status indicator (* for uncommitted changes)
  • Model Information: Shows active Claude model name
  • Output Style: Displays custom output style if configured
  • Dual Mode Support: Pro plan blocks or API session tracking
  • Intelligent Usage Indicators: Quota-aware burn rate indicators
  • Performance Optimized: Fast bash implementation with minimal subprocess calls
  • Color Coding: Uses ANSI escape codes for syntax highlighting

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

This is a bash script that generates a status line for the Claude Code CLI. It displays workspace information including directory name, git branch status, active Claude model, output style, and usage metrics (cost, burn rate, time remaining).

Architecture

The project consists of:

  • statusline-command.sh: Main bash script that reads JSON input from stdin and outputs formatted status line with ANSI color codes
  • statusline.go: Legacy Go implementation (kept for reference)
  • Single-file shell script architecture optimized for performance

Key Components

  • Input parsing: Reads JSON containing workspace, model, and output style information
  • Git integration: Detects git repositories and shows branch status with dirty state indicators
  • Usage metrics: Calls ccusage blocks --active --json for Pro plan or ccusage session --json for API usage
  • Mode support: Supports --mode=pro (default) and --mode=api for different usage tracking
  • Quota-aware burn rate: Uses intelligent emoji indicators (🟢🟡🟠🔥🚨) based on usage intensity
  • ANSI formatting: Uses escape codes for colored terminal output

Build Commands

Using Makefile (Recommended)

# Run tests with different modes and input styles
make test

# Install to ~/.claude directory
make install

# Clean build artifacts
make clean

# Test (default target)
make

Direct Script Usage

# Run the bash script directly
./statusline-command.sh < input.json

# With mode flag
./statusline-command.sh --mode=api < input.json

Dependencies

  • Bash shell
  • jq (for JSON parsing)
  • bc (for floating-point calculations)
  • Git (for git status information)
  • ccusage command (for Claude Code usage metrics)
  • External commands: git branch --show-current, git diff --quiet, git diff --cached --quiet

Data Flow

  1. Reads JSON input from stdin containing workspace info, model details, and output style
  2. Extracts current directory (prefers workspace.current_dir over cwd fallback)
  3. Gets git branch and status using git commands
  4. Fetches active block metrics using ccusage command
  5. Formats and outputs colored status line to stdout

Testing

Use the Makefile test target for comprehensive testing with timing measurements:

make test

Or test manually:

echo '{"workspace":{"current_dir":"/path"},"model":{"display_name":"Claude 3.5 Sonnet"},"output_style":{"name":"custom"}}' | ./statusline-command.sh --mode=pro
.PHONY: clean test install
# Script name
SCRIPT_NAME=statusline-command.sh
# Clean build artifacts
clean:
rm -f statusline
# Test with different modes and input files
test:
@echo "Testing Pro mode with Learning output style:"
@time (cat test-input.json | ./$(SCRIPT_NAME) --mode=pro)
@echo ""
@echo "Testing API mode with Learning output style:"
@time (cat test-input.json | ./$(SCRIPT_NAME) --mode=api)
@echo ""
@echo "Testing Pro mode with Explanatory output style:"
@time (cat test-input-explanatory.json | ./$(SCRIPT_NAME) --mode=pro)
@echo ""
@echo "Testing Pro mode with default output style:"
@time (cat test-input-default-style.json | ./$(SCRIPT_NAME))
# Install to ~/.claude directory
install:
mkdir -p ~/.claude
cp $(SCRIPT_NAME) ~/.claude/statusline
chmod +x ~/.claude/statusline
# Default target
all: test
#!/bin/bash
# Default mode
mode="pro"
# Parse command line flags
while [[ $# -gt 0 ]]; do
case $1 in
--mode=*)
mode="${1#*=}"
shift
;;
--mode)
mode="$2"
shift 2
;;
*)
echo "Unknown option: $1" >&2
exit 1
;;
esac
done
# Read JSON input from stdin
input=$(cat)
# Extract data from JSON input in one jq call
if ! json_data=$(echo "$input" | jq -r '[.workspace.current_dir // .cwd // "", .model.display_name // .model.id // "Claude", .session_id // "", .transcript_path // "", .output_style.name // ""] | @tsv' 2>/dev/null); then
current_dir=$(pwd 2>/dev/null || echo "")
model_name="Claude"
session_id=""
transcript_path=""
output_style=""
else
IFS=$'\t' read -r current_dir model_name session_id transcript_path output_style <<< "$json_data"
fi
# Get directory name and check if it's a git repo
dir_name=$(basename "$current_dir")
git_info=""
if [[ -d "$current_dir/.git" ]]; then
cd "$current_dir" 2>/dev/null || true
branch=$(git branch --show-current 2>/dev/null)
if [[ -n "$branch" ]]; then
# Check for uncommitted changes
if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
git_info="on $(printf '\033[91m%s\033[2m*\033[0m' "$branch")"
else
git_info="on $(printf '\033[92m%s\033[0m' "$branch")"
fi
fi
fi
# Get metrics based on mode
metrics_parts=()
if [[ "$mode" == "api" ]]; then
# Get session metrics for API mode
session_data=$(ccusage session --json 2>/dev/null)
if [[ -n "$session_data" && "$session_data" != "null" ]]; then
# Extract the first (most recent) session
session=$(echo "$session_data" | jq -r '.sessions[0]')
if [[ -n "$session" && "$session" != "null" ]]; then
# Extract session data in one jq call
session_metrics=$(echo "$session" | jq -r '[.totalCost // 0, .inputTokens // 0, .outputTokens // 0] | @tsv')
IFS=$'\t' read -r session_cost_raw input_tokens output_tokens <<< "$session_metrics"
# Format session cost
if [[ -n "$session_cost_raw" && "$session_cost_raw" != "null" && "$session_cost_raw" != "0" ]]; then
cost_cents=$(echo "$session_cost_raw * 100" | bc 2>/dev/null | cut -d. -f1)
if [[ $cost_cents -lt 100 ]]; then
session_cost="${cost_cents}¢"
else
dollars=$((cost_cents / 100))
cents=$((cost_cents % 100))
if [[ $cents -eq 0 ]]; then
session_cost="\$${dollars}"
else
session_cost="\$${dollars}.$(printf "%02d" $cents)"
fi
fi
else
session_cost="<1¢"
fi
# Calculate total tokens
total_tokens=$((input_tokens + output_tokens))
if [[ $total_tokens -gt 0 ]]; then
# Rough estimate: ~2k tokens per request
request_count=$(((total_tokens + 1999) / 2000))
request_display="~${request_count}"
else
request_display=""
fi
# Add session metrics to parts array
if [[ -n "$session_cost" ]]; then
metrics_parts+=("Session: $session_cost")
fi
if [[ -n "$request_display" ]]; then
metrics_parts+=("${request_display} requests")
fi
fi
fi
else
# Get block metrics for Pro mode (default)
block_cost=""
burn_rate=""
time_remaining=""
# Get active block data
active_block=$(ccusage blocks --active --json 2>/dev/null | jq -r '.blocks[] | select(.isActive == true)')
if [[ -n "$active_block" && "$active_block" != "null" ]]; then
# Extract all block data in one jq call
block_metrics=$(echo "$active_block" | jq -r '[.costUSD // 0, .burnRate.costPerHour // 0, .projection.remainingMinutes // 0] | @tsv')
IFS=$'\t' read -r block_cost_raw burn_rate_raw remaining_min <<< "$block_metrics"
# Format block cost
if [[ -n "$block_cost_raw" && "$block_cost_raw" != "null" && "$block_cost_raw" != "0" ]]; then
cost_cents=$(echo "$block_cost_raw * 100" | bc 2>/dev/null | cut -d. -f1)
if [[ $cost_cents -lt 100 ]]; then
block_cost="${cost_cents}¢"
else
dollars=$((cost_cents / 100))
cents=$((cost_cents % 100))
if [[ $cents -eq 0 ]]; then
block_cost="\$${dollars}"
else
block_cost="\$${dollars}.$(printf "%02d" $cents)"
fi
fi
fi
# Format burn rate with quota-aware indicators
if [[ -n "$burn_rate_raw" && "$burn_rate_raw" != "null" && "$burn_rate_raw" != "0" ]]; then
# Calculate quota intensity using arithmetic (faster than bc)
# Sustainable rate: $100 / (30 days * 24 hours) = 0.138888...
# Use integer math: multiply by 1000 for precision
sustainable_rate_thousandths=138 # 0.138888 * 1000 ≈ 138
burn_rate_thousandths=$(echo "$burn_rate_raw * 1000" | bc 2>/dev/null | cut -d. -f1)
# Choose emoji based on intensity
if [[ $burn_rate_thousandths -le 35 ]]; then # 0-25%
emoji="🟢"
elif [[ $burn_rate_thousandths -le 69 ]]; then # 25-50%
emoji="🟡"
elif [[ $burn_rate_thousandths -le 104 ]]; then # 50-75%
emoji="🟠"
elif [[ $burn_rate_thousandths -le 138 ]]; then # 75-100%
emoji="🔥"
else
emoji="🚨" # Over-quota
fi
burn_cents=$(echo "$burn_rate_raw * 100" | bc 2>/dev/null | cut -d. -f1)
if [[ $burn_cents -lt 100 ]]; then
burn_rate="${emoji} ${burn_cents}¢/hr"
else
burn_dollars=$((burn_cents / 100))
burn_cents_remainder=$((burn_cents % 100))
if [[ $burn_cents_remainder -eq 0 ]]; then
burn_rate="${emoji} \$${burn_dollars}/hr"
else
burn_rate="${emoji} \$${burn_dollars}.$(printf "%02d" $burn_cents_remainder)/hr"
fi
fi
fi
# Format time remaining
if [[ -n "$remaining_min" && "$remaining_min" != "null" && "$remaining_min" != "0" ]]; then
hours=$((remaining_min / 60))
minutes=$((remaining_min % 60))
if [[ $hours -gt 0 ]]; then
time_remaining="${hours}h${minutes}m"
else
time_remaining="${minutes}m"
fi
fi
fi
# Fallback if no active block
if [[ -z "$block_cost" ]]; then
block_cost="<1¢"
fi
# Add block metrics to parts array
if [[ -n "$block_cost" ]]; then
metrics_parts+=("$block_cost")
fi
if [[ -n "$burn_rate" ]]; then
metrics_parts+=("$burn_rate")
fi
if [[ -n "$time_remaining" ]]; then
metrics_parts+=("${time_remaining} left")
fi
fi
# Build Starship-inspired status line
# printf '\033[2m\033[96m╭─\033[0m '
# Directory with git info
if [[ -n "$git_info" ]]; then
printf '\033[2m\033[94m%s\033[0m %s ' "$dir_name" "$git_info"
else
printf '\033[2m\033[94m%s\033[0m ' "$dir_name"
fi
# Model info
printf '\033[2mvia\033[0m \033[35m%s\033[0m' "$model_name"
# Output style if not default
if [[ -n "$output_style" && "$output_style" != "default" ]]; then
printf ' \033[2min\033[0m \033[33m%s\033[0m' "$output_style"
fi
# Display metrics
for part in "${metrics_parts[@]}"; do
if [[ "$mode" == "api" ]]; then
# API mode uses different colors
if [[ "$part" == *"Session:"* ]]; then
printf ' \033[2m│\033[0m \033[32m%s\033[0m' "$part"
elif [[ "$part" == *"requests"* ]]; then
printf ' \033[2m│\033[0m \033[36m%s\033[0m' "$part"
fi
else
# Pro mode colors
if [[ "$part" == *"¢"* || "$part" == *"\$"* ]] && [[ "$part" != *"hr"* ]]; then
# Block cost
printf ' \033[2m│\033[0m \033[32m%s\033[0m' "$part"
elif [[ "$part" == *"/hr"* ]]; then
# Burn rate (already has emoji and colors)
printf ' \033[2m│\033[0m \033[33m%s\033[0m' "$part"
elif [[ "$part" == *"left"* ]]; then
# Time remaining
printf ' \033[2m│\033[0m \033[36m%s\033[2m\033[0m' "$part"
fi
fi
done
# printf '\n\033[2m\033[96m╰─\033[0m '
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
)
type Input struct {
Workspace struct {
CurrentDir string `json:"current_dir"`
} `json:"workspace"`
CWD string `json:"cwd"`
Model struct {
DisplayName string `json:"display_name"`
ID string `json:"id"`
} `json:"model"`
SessionID string `json:"session_id"`
TranscriptPath string `json:"transcript_path"`
OutputStyle struct {
Name string `json:"name"`
} `json:"output_style"`
}
type Block struct {
IsActive bool `json:"isActive"`
CostUSD float64 `json:"costUSD"`
BurnRate struct {
CostPerHour float64 `json:"costPerHour"`
} `json:"burnRate"`
Projection struct {
RemainingMinutes int `json:"remainingMinutes"`
} `json:"projection"`
}
type BlocksResponse struct {
Blocks []Block `json:"blocks"`
}
type Session struct {
SessionID string `json:"sessionId"`
TotalCost float64 `json:"totalCost"`
InputTokens int `json:"inputTokens"`
OutputTokens int `json:"outputTokens"`
LastActivity string `json:"lastActivity"`
}
type SessionsResponse struct {
Sessions []Session `json:"sessions"`
}
func main() {
// Parse command line flags
mode := flag.String("mode", "pro", "Mode: 'pro' for Pro plan with blocks, 'api' for API usage")
flag.Parse()
input, err := io.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err)
os.Exit(1)
}
var data Input
if err := json.Unmarshal(input, &data); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing JSON: %v\n", err)
os.Exit(1)
}
// Extract data
currentDir := data.Workspace.CurrentDir
if currentDir == "" {
currentDir = data.CWD
}
modelName := data.Model.DisplayName
if modelName == "" {
modelName = data.Model.ID
}
if modelName == "" {
modelName = "Claude"
}
outputStyle := data.OutputStyle.Name
// Get directory name and git info
dirName := filepath.Base(currentDir)
gitInfo := getGitInfo(currentDir)
// Get metrics based on mode
var metricsParts []string
if *mode == "api" {
sessionCost, requestCount := getSessionMetrics()
if sessionCost != "" {
metricsParts = append(metricsParts, fmt.Sprintf("\033[32mSession: %s\033[0m", sessionCost))
}
if requestCount != "" {
metricsParts = append(metricsParts, fmt.Sprintf("\033[36m%s requests\033[0m", requestCount))
}
} else {
blockCost, burnRate, timeRemaining := getBlockMetrics()
if blockCost != "" {
metricsParts = append(metricsParts, fmt.Sprintf("\033[32m%s\033[0m", blockCost))
}
if burnRate != "" {
metricsParts = append(metricsParts, fmt.Sprintf("%s", burnRate))
}
if timeRemaining != "" {
metricsParts = append(metricsParts, fmt.Sprintf("\033[36m%s\033[2m left\033[0m", timeRemaining))
}
}
// Build status line
if gitInfo != "" {
fmt.Printf("\033[2m\033[94m%s\033[0m %s ", dirName, gitInfo)
} else {
fmt.Printf("\033[2m\033[94m%s\033[0m ", dirName)
}
fmt.Printf("\033[2mvia\033[0m \033[35m%s\033[0m", modelName)
if outputStyle != "" && outputStyle != "default" {
fmt.Printf(" \033[2min\033[0m \033[33m%s\033[0m", outputStyle)
}
// Display metrics
for _, part := range metricsParts {
fmt.Printf(" \033[2m│\033[0m %s", part)
}
}
func getGitInfo(currentDir string) string {
gitDir := filepath.Join(currentDir, ".git")
if _, err := os.Stat(gitDir); os.IsNotExist(err) {
return ""
}
cmd := exec.Command("git", "branch", "--show-current")
cmd.Dir = currentDir
output, err := cmd.Output()
if err != nil {
return ""
}
branch := strings.TrimSpace(string(output))
if branch == "" {
return ""
}
// Check for uncommitted changes
diffCmd := exec.Command("git", "diff", "--quiet")
diffCmd.Dir = currentDir
diffErr := diffCmd.Run()
cachedCmd := exec.Command("git", "diff", "--cached", "--quiet")
cachedCmd.Dir = currentDir
cachedErr := cachedCmd.Run()
if diffErr != nil || cachedErr != nil {
return fmt.Sprintf("on \033[91m%s\033[2m*\033[0m", branch)
}
return fmt.Sprintf("on \033[92m%s\033[0m", branch)
}
func getBlockMetrics() (blockCost, burnRate, timeRemaining string) {
cmd := exec.Command("ccusage", "blocks", "--active", "--json")
output, err := cmd.Output()
if err != nil {
return "<1¢", "", ""
}
var response BlocksResponse
if err := json.Unmarshal(output, &response); err != nil {
return "<1¢", "", ""
}
var activeBlock *Block
for _, block := range response.Blocks {
if block.IsActive {
activeBlock = &block
break
}
}
if activeBlock == nil {
return "<1¢", "", ""
}
// Format block cost
if activeBlock.CostUSD > 0 {
costCents := int(activeBlock.CostUSD * 100)
if costCents < 100 {
blockCost = fmt.Sprintf("%d¢", costCents)
} else {
dollars := costCents / 100
cents := costCents % 100
if cents == 0 {
blockCost = fmt.Sprintf("$%d", dollars)
} else {
blockCost = fmt.Sprintf("$%d.%02d", dollars, cents)
}
}
} else {
blockCost = "<1¢"
}
// Format burn rate with quota-aware indicators
if activeBlock.BurnRate.CostPerHour > 0 {
burnCents := int(activeBlock.BurnRate.CostPerHour * 100)
// Calculate quota intensity (assuming $100 Pro plan, 30-day blocks)
// Sustainable rate: $100 / (30 days * 24 hours) ≈ $0.139/hr
sustainableRatePerHour := 100.0 / (30.0 * 24.0) // $0.139/hr
intensity := activeBlock.BurnRate.CostPerHour / sustainableRatePerHour
var emoji string
if intensity <= 0.25 {
emoji = "🟢" // Low burn (0-25%)
} else if intensity <= 0.50 {
emoji = "🟡" // Moderate burn (25-50%)
} else if intensity <= 0.75 {
emoji = "🟠" // High burn (50-75%)
} else if intensity <= 1.0 {
emoji = "🔥" // Critical burn (75-100%)
} else {
emoji = "🚨" // Over-quota burn (>100%)
}
if burnCents < 100 {
burnRate = fmt.Sprintf("\033[33m%s %d¢/hr\033[0m", emoji, burnCents)
} else {
dollars := burnCents / 100
cents := burnCents % 100
if cents == 0 {
burnRate = fmt.Sprintf("\033[33m%s $%d/hr\033[0m", emoji, dollars)
} else {
burnRate = fmt.Sprintf("\033[33m%s $%d.%02d/hr\033[0m", emoji, dollars, cents)
}
}
}
// Format time remaining
if activeBlock.Projection.RemainingMinutes > 0 {
hours := activeBlock.Projection.RemainingMinutes / 60
minutes := activeBlock.Projection.RemainingMinutes % 60
if hours > 0 {
timeRemaining = fmt.Sprintf("%dh%dm", hours, minutes)
} else {
timeRemaining = fmt.Sprintf("%dm", minutes)
}
}
return blockCost, burnRate, timeRemaining
}
func getSessionMetrics() (sessionCost, requestCount string) {
cmd := exec.Command("ccusage", "session", "--json")
output, err := cmd.Output()
if err != nil {
return "<1¢", ""
}
var response SessionsResponse
if err := json.Unmarshal(output, &response); err != nil {
return "<1¢", ""
}
// Find the most recent session (assuming they're sorted by last activity)
var currentSession *Session
if len(response.Sessions) > 0 {
// Use the first session (most recent if sorted)
currentSession = &response.Sessions[0]
}
if currentSession == nil {
return "<1¢", ""
}
// Format session cost
if currentSession.TotalCost > 0 {
costCents := int(currentSession.TotalCost * 100)
if costCents < 100 {
sessionCost = fmt.Sprintf("%d¢", costCents)
} else {
dollars := costCents / 100
cents := costCents % 100
if cents == 0 {
sessionCost = fmt.Sprintf("$%d", dollars)
} else {
sessionCost = fmt.Sprintf("$%d.%02d", dollars, cents)
}
}
} else {
sessionCost = "<1¢"
}
// Format request count using tokens as proxy (input + output tokens)
totalTokens := currentSession.InputTokens + currentSession.OutputTokens
if totalTokens > 0 {
// Convert tokens to approximate "requests" - rough estimate
requestCount = fmt.Sprintf("~%d", (totalTokens+1999)/2000) // Assume ~2k tokens per request
}
return sessionCost, requestCount
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment