Last active
March 19, 2026 16:51
-
-
Save solrz/f0f6faec4227df5c23b5d0a537b39dc4 to your computer and use it in GitHub Desktop.
Claude Code Statusline - Shows context usage (C:), 5-hour API usage (S:), and weekly usage remaining (W:) with color gradients
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env swift | |
| import Foundation | |
| func readSessionKey() -> String? { | |
| // TODO: Replace with your session key from claude.ai cookies | |
| let injectedKey = "YOUR_SESSION_KEY_HERE" | |
| let trimmedKey = injectedKey.trimmingCharacters(in: .whitespacesAndNewlines) | |
| return trimmedKey.isEmpty ? nil : trimmedKey | |
| } | |
| func readOrganizationId() -> String? { | |
| // TODO: Replace with your organization ID from claude.ai | |
| let injectedOrgId = "YOUR_ORG_ID_HERE" | |
| let trimmedOrgId = injectedOrgId.trimmingCharacters(in: .whitespacesAndNewlines) | |
| return trimmedOrgId.isEmpty ? nil : trimmedOrgId | |
| } | |
| func fetchUsageData(sessionKey: String, orgId: String) async throws -> (utilization: Int, resetsAt: String?, weeklyUtilization: Int?, weeklyResetsAt: String?) { | |
| // Build URL safely - validate orgId doesn't contain path traversal | |
| guard !orgId.contains(".."), !orgId.contains("/") else { | |
| throw NSError(domain: "ClaudeAPI", code: 5, userInfo: [NSLocalizedDescriptionKey: "Invalid organization ID"]) | |
| } | |
| guard let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/usage") else { | |
| throw NSError(domain: "ClaudeAPI", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]) | |
| } | |
| var request = URLRequest(url: url) | |
| request.setValue("sessionKey=\(sessionKey)", forHTTPHeaderField: "Cookie") | |
| request.setValue("application/json", forHTTPHeaderField: "Accept") | |
| request.httpMethod = "GET" | |
| let (data, response) = try await URLSession.shared.data(for: request) | |
| guard let httpResponse = response as? HTTPURLResponse, | |
| httpResponse.statusCode == 200 else { | |
| throw NSError(domain: "ClaudeAPI", code: 3, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch usage"]) | |
| } | |
| if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], | |
| let fiveHour = json["five_hour"] as? [String: Any], | |
| let utilization = fiveHour["utilization"] as? Double { | |
| let resetsAt = fiveHour["resets_at"] as? String | |
| // Extract seven_day (weekly) data if available | |
| var weeklyUtilization: Int? = nil | |
| var weeklyResetsAt: String? = nil | |
| if let sevenDay = json["seven_day"] as? [String: Any] { | |
| if let weeklyUtil = sevenDay["utilization"] as? Double { | |
| weeklyUtilization = Int(weeklyUtil) | |
| } | |
| weeklyResetsAt = sevenDay["resets_at"] as? String | |
| } | |
| return (Int(utilization), resetsAt, weeklyUtilization, weeklyResetsAt) | |
| } | |
| throw NSError(domain: "ClaudeAPI", code: 4, userInfo: [NSLocalizedDescriptionKey: "Invalid response format"]) | |
| } | |
| // Main execution | |
| // Use Task to run async code, RunLoop keeps script alive until exit() is called | |
| Task { | |
| guard let sessionKey = readSessionKey() else { | |
| print("ERROR:NO_SESSION_KEY") | |
| exit(1) | |
| } | |
| guard let orgId = readOrganizationId() else { | |
| print("ERROR:NO_ORG_CONFIGURED") | |
| exit(1) | |
| } | |
| do { | |
| let (utilization, resetsAt, weeklyUtilization, weeklyResetsAt) = try await fetchUsageData(sessionKey: sessionKey, orgId: orgId) | |
| // Output format: UTILIZATION|RESETS_AT|WEEKLY_UTILIZATION|WEEKLY_RESETS_AT | |
| let resets = resetsAt ?? "" | |
| let weeklyUtil = weeklyUtilization.map { String($0) } ?? "" | |
| let weeklyResets = weeklyResetsAt ?? "" | |
| print("\(utilization)|\(resets)|\(weeklyUtil)|\(weeklyResets)") | |
| exit(0) | |
| } catch { | |
| print("ERROR:\(error.localizedDescription)") | |
| exit(1) | |
| } | |
| } | |
| // Keep script alive while async Task executes | |
| RunLoop.main.run() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| config_file="$HOME/.claude/statusline-config.txt" | |
| if [ -f "$config_file" ]; then | |
| source "$config_file" | |
| show_dir=$SHOW_DIRECTORY | |
| show_branch=$SHOW_BRANCH | |
| show_usage=$SHOW_USAGE | |
| show_bar=$SHOW_PROGRESS_BAR | |
| show_reset=$SHOW_RESET_TIME | |
| show_context=$SHOW_CONTEXT | |
| else | |
| show_dir=1 | |
| show_branch=1 | |
| show_usage=1 | |
| show_bar=1 | |
| show_reset=1 | |
| show_context=1 | |
| fi | |
| input=$(cat) | |
| current_dir_path=$(echo "$input" | grep -o '"current_dir":"[^"]*"' | sed 's/"current_dir":"//;s/"$//') | |
| current_dir=$(basename "$current_dir_path") | |
| BLUE=$'\033[0;34m' | |
| GREEN=$'\033[0;32m' | |
| GRAY=$'\033[0;90m' | |
| YELLOW=$'\033[0;33m' | |
| RESET=$'\033[0m' | |
| # 10-level gradient: dark green → deep red | |
| LEVEL_1=$'\033[38;5;22m' # dark green | |
| LEVEL_2=$'\033[38;5;28m' # soft green | |
| LEVEL_3=$'\033[38;5;34m' # medium green | |
| LEVEL_4=$'\033[38;5;100m' # green-yellowish dark | |
| LEVEL_5=$'\033[38;5;142m' # olive/yellow-green dark | |
| LEVEL_6=$'\033[38;5;178m' # muted yellow | |
| LEVEL_7=$'\033[38;5;172m' # muted yellow-orange | |
| LEVEL_8=$'\033[38;5;166m' # darker orange | |
| LEVEL_9=$'\033[38;5;160m' # dark red | |
| LEVEL_10=$'\033[38;5;124m' # deep red | |
| # Build components (without separators) | |
| dir_text="" | |
| if [ "$show_dir" = "1" ]; then | |
| dir_text="${BLUE}${current_dir}${RESET}" | |
| fi | |
| branch_text="" | |
| if [ "$show_branch" = "1" ]; then | |
| if git rev-parse --git-dir > /dev/null 2>&1; then | |
| branch=$(git branch --show-current 2>/dev/null) | |
| [ -n "$branch" ] && branch_text="${GREEN}⎇ ${branch}${RESET}" | |
| fi | |
| fi | |
| context_text="" | |
| if [ "$show_context" = "1" ]; then | |
| # Parse context_window.used_percentage from JSON input | |
| context_used=$(echo "$input" | grep -o '"used_percentage":[0-9.]*' | sed 's/"used_percentage"://') | |
| if [ -n "$context_used" ]; then | |
| # Convert to integer for comparison (truncate decimal) | |
| context_raw=${context_used%.*} | |
| [ -z "$context_raw" ] && context_raw=0 | |
| # Scale to 100% (context full at 85%) | |
| context_int=$(( context_raw * 100 / 85 )) | |
| [ "$context_int" -gt 100 ] && context_int=100 | |
| # Select color based on usage level (same gradient as API usage) | |
| if [ "$context_int" -le 10 ]; then | |
| context_color="$LEVEL_1" | |
| elif [ "$context_int" -le 20 ]; then | |
| context_color="$LEVEL_2" | |
| elif [ "$context_int" -le 30 ]; then | |
| context_color="$LEVEL_3" | |
| elif [ "$context_int" -le 40 ]; then | |
| context_color="$LEVEL_4" | |
| elif [ "$context_int" -le 50 ]; then | |
| context_color="$LEVEL_5" | |
| elif [ "$context_int" -le 60 ]; then | |
| context_color="$LEVEL_6" | |
| elif [ "$context_int" -le 70 ]; then | |
| context_color="$LEVEL_7" | |
| elif [ "$context_int" -le 80 ]; then | |
| context_color="$LEVEL_8" | |
| elif [ "$context_int" -le 90 ]; then | |
| context_color="$LEVEL_9" | |
| else | |
| context_color="$LEVEL_10" | |
| fi | |
| if [ "$show_bar" = "1" ]; then | |
| if [ "$context_int" -eq 0 ]; then | |
| ctx_filled=0 | |
| elif [ "$context_int" -ge 100 ]; then | |
| ctx_filled=10 | |
| else | |
| ctx_filled=$(( (context_int * 10 + 50) / 100 )) | |
| fi | |
| [ "$ctx_filled" -lt 0 ] && ctx_filled=0 | |
| [ "$ctx_filled" -gt 10 ] && ctx_filled=10 | |
| ctx_empty=$((10 - ctx_filled)) | |
| ctx_bar=" " | |
| i=0 | |
| while [ $i -lt $ctx_filled ]; do | |
| ctx_bar="${ctx_bar}▓" | |
| i=$((i + 1)) | |
| done | |
| i=0 | |
| while [ $i -lt $ctx_empty ]; do | |
| ctx_bar="${ctx_bar}░" | |
| i=$((i + 1)) | |
| done | |
| else | |
| ctx_bar="" | |
| fi | |
| context_text="${context_color}C: ${context_int}%${ctx_bar}${RESET}" | |
| fi | |
| fi | |
| # Parse token usage and calculate cost | |
| token_text="" | |
| total_input=$(echo "$input" | grep -o '"total_input_tokens":[0-9]*' | sed 's/"total_input_tokens"://') | |
| total_output=$(echo "$input" | grep -o '"total_output_tokens":[0-9]*' | sed 's/"total_output_tokens"://') | |
| if [ -n "$total_input" ] && [ -n "$total_output" ]; then | |
| # Format as K (thousands) | |
| input_k=$((total_input / 1000)) | |
| output_k=$((total_output / 1000)) | |
| # Calculate cost: input $5/1M + output $25/1M | |
| # Using cents: input * 500 / 1000000 + output * 2500 / 1000000 | |
| cost_cents=$(( (total_input * 500 + total_output * 2500) / 1000000 )) | |
| cost_dollars=$((cost_cents / 100)) | |
| cost_remainder=$((cost_cents % 100)) | |
| cost_display=$(printf "\$%d.%02d" "$cost_dollars" "$cost_remainder") | |
| token_text="${GRAY}${input_k}K/${output_k}K ${cost_display}${RESET}" | |
| fi | |
| usage_text="" | |
| weekly_text="" | |
| if [ "$show_usage" = "1" ]; then | |
| swift_result=$(swift "$HOME/.claude/fetch-claude-usage.swift" 2>/dev/null) | |
| if [ $? -eq 0 ] && [ -n "$swift_result" ]; then | |
| utilization=$(echo "$swift_result" | cut -d'|' -f1) | |
| resets_at=$(echo "$swift_result" | cut -d'|' -f2) | |
| weekly_util=$(echo "$swift_result" | cut -d'|' -f3) | |
| weekly_resets=$(echo "$swift_result" | cut -d'|' -f4) | |
| if [ -n "$utilization" ] && [ "$utilization" != "ERROR" ]; then | |
| if [ "$utilization" -le 10 ]; then | |
| usage_color="$LEVEL_1" | |
| elif [ "$utilization" -le 20 ]; then | |
| usage_color="$LEVEL_2" | |
| elif [ "$utilization" -le 30 ]; then | |
| usage_color="$LEVEL_3" | |
| elif [ "$utilization" -le 40 ]; then | |
| usage_color="$LEVEL_4" | |
| elif [ "$utilization" -le 50 ]; then | |
| usage_color="$LEVEL_5" | |
| elif [ "$utilization" -le 60 ]; then | |
| usage_color="$LEVEL_6" | |
| elif [ "$utilization" -le 70 ]; then | |
| usage_color="$LEVEL_7" | |
| elif [ "$utilization" -le 80 ]; then | |
| usage_color="$LEVEL_8" | |
| elif [ "$utilization" -le 90 ]; then | |
| usage_color="$LEVEL_9" | |
| else | |
| usage_color="$LEVEL_10" | |
| fi | |
| if [ "$show_bar" = "1" ]; then | |
| if [ "$utilization" -eq 0 ]; then | |
| filled_blocks=0 | |
| elif [ "$utilization" -eq 100 ]; then | |
| filled_blocks=10 | |
| else | |
| filled_blocks=$(( (utilization * 10 + 50) / 100 )) | |
| fi | |
| [ "$filled_blocks" -lt 0 ] && filled_blocks=0 | |
| [ "$filled_blocks" -gt 10 ] && filled_blocks=10 | |
| empty_blocks=$((10 - filled_blocks)) | |
| # Build progress bar safely without seq | |
| progress_bar=" " | |
| i=0 | |
| while [ $i -lt $filled_blocks ]; do | |
| progress_bar="${progress_bar}▓" | |
| i=$((i + 1)) | |
| done | |
| i=0 | |
| while [ $i -lt $empty_blocks ]; do | |
| progress_bar="${progress_bar}░" | |
| i=$((i + 1)) | |
| done | |
| else | |
| progress_bar="" | |
| fi | |
| reset_time_display="" | |
| if [ "$show_reset" = "1" ] && [ -n "$resets_at" ] && [ "$resets_at" != "null" ]; then | |
| # Strip milliseconds and timezone (handles both Z and +00:00 formats) | |
| iso_time=$(echo "$resets_at" | sed 's/\.[0-9]*//; s/Z$//; s/[+-][0-9][0-9]:[0-9][0-9]$//') | |
| epoch=$(date -ju -f "%Y-%m-%dT%H:%M:%S" "$iso_time" "+%s" 2>/dev/null) | |
| if [ -n "$epoch" ]; then | |
| # Detect system time format (12h vs 24h) from macOS locale preferences | |
| time_format=$(defaults read -g AppleICUForce24HourTime 2>/dev/null) | |
| if [ "$time_format" = "1" ]; then | |
| # 24-hour format | |
| reset_time=$(date -r "$epoch" "+%H:%M" 2>/dev/null) | |
| else | |
| # 12-hour format (default) | |
| reset_time=$(date -r "$epoch" "+%I:%M %p" 2>/dev/null) | |
| fi | |
| [ -n "$reset_time" ] && reset_time_display=$(printf " → Reset: %s" "$reset_time") | |
| fi | |
| fi | |
| usage_text="${usage_color}S: ${utilization}%${progress_bar}${reset_time_display}${RESET}" | |
| # Calculate weekly display if available | |
| if [ -n "$weekly_util" ] && [ "$weekly_util" != "" ]; then | |
| # weekly_left = 8 * (100 - weekly_util) => 800% when 0%, 0% when 100% | |
| weekly_left=$(( 8 * (100 - weekly_util) )) | |
| # Calculate time remaining until weekly reset | |
| time_pct=0 | |
| if [ -n "$weekly_resets" ] && [ "$weekly_resets" != "" ]; then | |
| # Strip milliseconds and timezone (handles both Z and +00:00 formats) | |
| weekly_iso=$(echo "$weekly_resets" | sed 's/\.[0-9]*//; s/Z$//; s/[+-][0-9][0-9]:[0-9][0-9]$//') | |
| weekly_epoch=$(date -ju -f "%Y-%m-%dT%H:%M:%S" "$weekly_iso" "+%s" 2>/dev/null) | |
| if [ -n "$weekly_epoch" ]; then | |
| now_epoch=$(date "+%s") | |
| seconds_left=$((weekly_epoch - now_epoch)) | |
| [ "$seconds_left" -lt 0 ] && seconds_left=0 | |
| minutes_left=$((seconds_left / 60)) | |
| # time_pct = minutes_left * 800 / 10080 (1440 * 7 = 10080 minutes per week) | |
| time_pct=$((minutes_left * 800 / 10080)) | |
| [ "$time_pct" -gt 800 ] && time_pct=800 | |
| fi | |
| fi | |
| # Color based on weekly_left (0-800 scale, map to 0-100 for color selection) | |
| weekly_color_level=$((weekly_left / 8)) | |
| if [ "$weekly_color_level" -ge 90 ]; then | |
| weekly_color="$LEVEL_1" | |
| elif [ "$weekly_color_level" -ge 80 ]; then | |
| weekly_color="$LEVEL_2" | |
| elif [ "$weekly_color_level" -ge 70 ]; then | |
| weekly_color="$LEVEL_3" | |
| elif [ "$weekly_color_level" -ge 60 ]; then | |
| weekly_color="$LEVEL_4" | |
| elif [ "$weekly_color_level" -ge 50 ]; then | |
| weekly_color="$LEVEL_5" | |
| elif [ "$weekly_color_level" -ge 40 ]; then | |
| weekly_color="$LEVEL_6" | |
| elif [ "$weekly_color_level" -ge 30 ]; then | |
| weekly_color="$LEVEL_7" | |
| elif [ "$weekly_color_level" -ge 20 ]; then | |
| weekly_color="$LEVEL_8" | |
| elif [ "$weekly_color_level" -ge 10 ]; then | |
| weekly_color="$LEVEL_9" | |
| else | |
| weekly_color="$LEVEL_10" | |
| fi | |
| weekly_text="${weekly_color}W: ${weekly_left}%:${time_pct}%${RESET}" | |
| fi | |
| else | |
| usage_text="${YELLOW}S: ~${RESET}" | |
| fi | |
| else | |
| usage_text="${YELLOW}S: ~${RESET}" | |
| fi | |
| fi | |
| output="" | |
| separator="${GRAY} │ ${RESET}" | |
| [ -n "$dir_text" ] && output="${dir_text}" | |
| if [ -n "$branch_text" ]; then | |
| [ -n "$output" ] && output="${output}${separator}" | |
| output="${output}${branch_text}" | |
| fi | |
| if [ -n "$context_text" ]; then | |
| [ -n "$output" ] && output="${output}${separator}" | |
| output="${output}${context_text}" | |
| fi | |
| if [ -n "$token_text" ]; then | |
| [ -n "$output" ] && output="${output}${separator}" | |
| output="${output}${token_text}" | |
| fi | |
| if [ -n "$usage_text" ]; then | |
| [ -n "$output" ] && output="${output}${separator}" | |
| output="${output}${usage_text}" | |
| fi | |
| if [ -n "$weekly_text" ]; then | |
| [ -n "$output" ] && output="${output}${separator}" | |
| output="${output}${weekly_text}" | |
| fi | |
| printf "%s\n" "$output" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment