- Download
.config__ccstatusline__settings.jsonto~/.config/ccstatusline/settings.json, replace and with your respective claude code email accounts - Download
bashScripts__ccstatusline-usage.shto~/bashScripts/ccstatusline-usage.sh
Last active
April 8, 2026 01:25
-
-
Save fcamblor/0ad17cab5cc797d9e92d0620be795f99 to your computer and use it in GitHub Desktop.
My custom Claude Code StatusLine settings (for Mac)
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
| { | |
| "version": 3, | |
| "lines": [ | |
| [ | |
| { | |
| "id": "0c199d1c-30ab-4750-889c-1f20bdfe0370", | |
| "type": "context-percentage", | |
| "backgroundColor": "bgYellow", | |
| "metadata": { | |
| "inverse": "false" | |
| } | |
| }, | |
| { | |
| "id": "3", | |
| "type": "context-length", | |
| "color": "brightBlack" | |
| }, | |
| { | |
| "id": "502d9f6f-2f4a-4dab-b698-f27b67e28e4c", | |
| "type": "model", | |
| "backgroundColor": "bgBrightBlue" | |
| }, | |
| { | |
| "id": "91fe07b8-6e3e-4fc4-b8f0-1e86e225fdbc", | |
| "type": "block-timer", | |
| "backgroundColor": "bgBrightMagenta" | |
| }, | |
| { | |
| "id": "b4e49d52-6c86-4d16-80d7-3540f3c03018", | |
| "type": "session-cost", | |
| "backgroundColor": "bgBrightWhite" | |
| }, | |
| { | |
| "id": "0f203b02-b843-44c0-bd24-a822e229197a", | |
| "type": "session-clock", | |
| "backgroundColor": "bgBrightYellow" | |
| } | |
| ], | |
| [ | |
| { | |
| "id": "123be3ac-5e8e-41df-800a-2ece070a2570", | |
| "type": "current-working-dir", | |
| "backgroundColor": "bgMagenta" | |
| }, | |
| { | |
| "id": "38c8a1a1-4e57-4549-8cf6-2d9299f4f5ed", | |
| "type": "git-worktree", | |
| "backgroundColor": "bgWhite" | |
| }, | |
| { | |
| "id": "ecb97f54-78ef-4af7-a47d-ef3de5c5bdcc", | |
| "type": "git-branch", | |
| "backgroundColor": "bgBrightBlack" | |
| }, | |
| { | |
| "id": "28fb19a1-785a-4420-a0ad-18b42638995c", | |
| "type": "git-changes", | |
| "backgroundColor": "bgBrightGreen" | |
| } | |
| ], | |
| [ | |
| { | |
| "id": "e3177294-eaeb-4bba-a26f-0adb004dcab6", | |
| "type": "custom-command", | |
| "backgroundColor": "bgWhite", | |
| "commandPath": "~/bashScripts/ccstatusline-usage.sh email --email <email1> --bracket" | |
| }, | |
| { | |
| "id": "da18f30f-3005-4505-a7d0-ac604703d4b0", | |
| "type": "custom-command", | |
| "backgroundColor": "bgBrightYellow", | |
| "commandPath": "~/bashScripts/ccstatusline-usage.sh ttl --email <email1>" | |
| }, | |
| { | |
| "id": "a19499a4-41f4-4f46-b8e7-e71314312d7c", | |
| "type": "custom-command", | |
| "backgroundColor": "bgBrightWhite", | |
| "commandPath": "~/bashScripts/ccstatusline-usage.sh session --email <email1>" | |
| }, | |
| { | |
| "id": "3b58aaf0-1d76-44d9-baf0-6e0b4723d3cd", | |
| "type": "custom-command", | |
| "backgroundColor": "bgBrightBlue", | |
| "commandPath": "~/bashScripts/ccstatusline-usage.sh weekly --email <email1>" | |
| } | |
| ], | |
| [ | |
| { | |
| "id": "49d535a6-56f8-42f5-b033-8a8bbbb6cd5c", | |
| "type": "custom-command", | |
| "backgroundColor": "bgBrightBlack", | |
| "commandPath": "~/bashScripts/ccstatusline-usage.sh email --email <email2> --bracket" | |
| }, | |
| { | |
| "id": "dc0a7aed-704a-47f9-8ef5-41755bde1950", | |
| "type": "custom-command", | |
| "backgroundColor": "bgBlue", | |
| "commandPath": "~/bashScripts/ccstatusline-usage.sh ttl --email <email2>" | |
| }, | |
| { | |
| "id": "56f00408-9c69-44eb-a06b-411d34b64de3", | |
| "type": "custom-command", | |
| "backgroundColor": "bgMagenta", | |
| "commandPath": "~/bashScripts/ccstatusline-usage.sh session --email <email2>" | |
| }, | |
| { | |
| "id": "79ed94da-4403-41d4-9cce-2420bb163c28", | |
| "type": "custom-command", | |
| "backgroundColor": "bgCyan", | |
| "commandPath": "~/bashScripts/ccstatusline-usage.sh weekly --email <email2>" | |
| } | |
| ] | |
| ], | |
| "flexMode": "full-minus-40", | |
| "compactThreshold": 60, | |
| "colorLevel": 3, | |
| "defaultPadding": " ", | |
| "inheritSeparatorColors": false, | |
| "globalBold": false, | |
| "powerline": { | |
| "enabled": true, | |
| "separators": [ | |
| "" | |
| ], | |
| "separatorInvertBackground": [ | |
| false | |
| ], | |
| "startCaps": [ | |
| "" | |
| ], | |
| "endCaps": [ | |
| "" | |
| ], | |
| "theme": "solarized", | |
| "autoAlign": false | |
| } | |
| } |
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 | |
| #set -x | |
| #set -e -o -u | |
| USAGES_FILE="$HOME/.cache/ccstatusline/usages.json" | |
| LOCK_FILE="$HOME/.cache/ccstatusline/usages.lock" | |
| # Function to generate progress bar | |
| make_bar() { | |
| local pct="$1" | |
| local width=15 | |
| local filled=$((pct * width / 100)) | |
| local empty=$((width - filled)) | |
| printf "[" | |
| for ((i=0; i<filled; i++)); do printf "█"; done | |
| for ((i=0; i<empty; i++)); do printf "░"; done | |
| printf "]" | |
| } | |
| # Format time delta as "Xd Yh Zm" (omitting zero values) | |
| format_delta() { | |
| local seconds="$1" | |
| local days=$((seconds / 86400)) | |
| local hours=$(((seconds % 86400) / 3600)) | |
| local minutes=$(((seconds % 3600) / 60)) | |
| local result="" | |
| [[ $days -gt 0 ]] && result="${result}${days}d " | |
| [[ $hours -gt 0 ]] && result="${result}${hours}h " | |
| [[ $minutes -gt 0 ]] && result="${result}${minutes}m" | |
| echo "${result% }" | |
| } | |
| # Convert ISO timestamp to epoch seconds (UTC) | |
| iso_to_epoch() { | |
| TZ=UTC0 date -j -f "%Y-%m-%dT%H:%M:%S" "${1:0:19}" "+%s" 2>/dev/null | |
| } | |
| # Format epoch to readable date (UTC) | |
| epoch_to_date() { | |
| TZ=UTC0 date -r "$1" "+%y-%m-%d %H:%M" 2>/dev/null | |
| } | |
| # Format epoch to readable date (Europe/Paris timezone) | |
| epoch_to_date_local() { | |
| TZ=Europe/Paris date -r "$1" "+%y-%m-%d %H:%M" 2>/dev/null | |
| } | |
| # Get current epoch | |
| now_epoch() { | |
| date +%s | |
| } | |
| # Save error for email in usages.json | |
| save_error_for_email() { | |
| local email="$1" | |
| local error_type="$2" | |
| if [[ -z "$email" || -z "$error_type" ]]; then return 1; fi | |
| mkdir -p "$(dirname "$USAGES_FILE")" | |
| NOW=$(date -u +"%Y-%m-%dT%H:%M:%S+00:00") | |
| if [[ -f "$USAGES_FILE" ]]; then | |
| EXISTING=$(jq -r --arg email "$email" '.usages[] | select(.email == $email) | .email // empty' "$USAGES_FILE" 2>/dev/null) | |
| if [[ -n "$EXISTING" ]]; then | |
| jq --arg email "$email" --arg now "$NOW" --arg error "$error_type" \ | |
| '(.usages[] | select(.email == $email)) |= . + {lastError: {timestamp: $now, type: $error}}' \ | |
| "$USAGES_FILE" > "${USAGES_FILE}.tmp" && mv "${USAGES_FILE}.tmp" "$USAGES_FILE" | |
| else | |
| jq --arg email "$email" --arg now "$NOW" --arg error "$error_type" \ | |
| '.usages += [{email: $email, lastError: {timestamp: $now, type: $error}}]' \ | |
| "$USAGES_FILE" > "${USAGES_FILE}.tmp" && mv "${USAGES_FILE}.tmp" "$USAGES_FILE" | |
| fi | |
| else | |
| jq -n --arg email "$email" --arg now "$NOW" --arg error "$error_type" \ | |
| '{usages: [{email: $email, lastError: {timestamp: $now, type: $error}}]}' > "$USAGES_FILE" | |
| fi | |
| } | |
| # Find the closest future session reset time across all emails | |
| get_closest_session_reset() { | |
| if [[ ! -f "$USAGES_FILE" ]]; then return 1; fi | |
| local now_epoch=$(now_epoch) | |
| local closest="" | |
| while IFS= read -r reset_time; do | |
| if [[ -n "$reset_time" ]]; then | |
| local reset_epoch=$(iso_to_epoch "$reset_time") | |
| if [[ -n "$reset_epoch" && $reset_epoch -gt $now_epoch ]]; then | |
| if [[ -z "$closest" || $reset_epoch -lt $closest ]]; then | |
| closest="$reset_epoch" | |
| fi | |
| fi | |
| fi | |
| done < <(jq -r '.usages[].usage.sessionResetAt' "$USAGES_FILE" 2>/dev/null) | |
| [[ -n "$closest" ]] && echo "$closest" | |
| } | |
| # Find the closest future weekly reset time across all emails | |
| get_closest_weekly_reset() { | |
| if [[ ! -f "$USAGES_FILE" ]]; then return 1; fi | |
| local now_epoch=$(now_epoch) | |
| local closest="" | |
| while IFS= read -r reset_time; do | |
| if [[ -n "$reset_time" ]]; then | |
| local reset_epoch=$(iso_to_epoch "$reset_time") | |
| if [[ -n "$reset_epoch" && $reset_epoch -gt $now_epoch ]]; then | |
| if [[ -z "$closest" || $reset_epoch -lt $closest ]]; then | |
| closest="$reset_epoch" | |
| fi | |
| fi | |
| fi | |
| done < <(jq -r '.usages[].usage.weeklyResetAt' "$USAGES_FILE" 2>/dev/null) | |
| [[ -n "$closest" ]] && echo "$closest" | |
| } | |
| # Format usage data from usages.json for the given email and output | |
| output_from_usages() { | |
| local email="$1" | |
| local entry | |
| entry=$(jq -r --arg email "$email" '.usages[] | select(.email == $email)' "$USAGES_FILE" 2>/dev/null) | |
| [[ -z "$entry" ]] && return 1 | |
| local session_int weekly_int session_reset weekly_reset | |
| session_int=$(echo "$entry" | jq -r '.usage.sessionUsage') | |
| weekly_int=$(echo "$entry" | jq -r '.usage.weeklyUsage') | |
| session_reset=$(echo "$entry" | jq -r '.usage.sessionResetAt') | |
| weekly_reset=$(echo "$entry" | jq -r '.usage.weeklyResetAt') | |
| local session_epoch weekly_epoch session_reset_fmt weekly_reset_fmt | |
| session_epoch=$(iso_to_epoch "$session_reset") | |
| session_reset_fmt=$(epoch_to_date_local "$session_epoch") | |
| weekly_epoch=$(iso_to_epoch "$weekly_reset") | |
| weekly_reset_fmt=$(epoch_to_date_local "$weekly_epoch") | |
| local now_epoch session_delta weekly_delta session_delta_fmt weekly_delta_fmt | |
| now_epoch=$(now_epoch) | |
| session_delta=$(( session_epoch - now_epoch )) | |
| weekly_delta=$(( weekly_epoch - now_epoch )) | |
| session_delta_fmt=$(format_delta "$session_delta") | |
| weekly_delta_fmt=$(format_delta "$weekly_delta") | |
| local session_bar weekly_bar | |
| session_bar=$(make_bar "$session_int") | |
| weekly_bar=$(make_bar "$weekly_int") | |
| # Check if this is the closest reset time for session/weekly | |
| local closest_session=$(get_closest_session_reset) | |
| local closest_weekly=$(get_closest_weekly_reset) | |
| local session_icon="" | |
| local weekly_icon="" | |
| if [[ "$session_epoch" == "$closest_session" ]]; then | |
| session_icon="⏳" | |
| fi | |
| if [[ "$weekly_epoch" == "$closest_weekly" ]]; then | |
| weekly_icon="⏳" | |
| fi | |
| local session_icon_fmt="" | |
| local weekly_icon_fmt="" | |
| [[ -n "$session_icon" ]] && session_icon_fmt=" $session_icon" | |
| [[ -n "$weekly_icon" ]] && weekly_icon_fmt=" $weekly_icon" | |
| local session_output="Session ($session_delta_fmt →$session_icon_fmt $session_reset_fmt): $session_bar ${session_int}%" | |
| local weekly_output="Weekly ($weekly_delta_fmt →$weekly_icon_fmt $weekly_reset_fmt): $weekly_bar ${weekly_int}%" | |
| if [[ "$SUBCOMMAND" == "session" ]]; then echo "$session_output" | |
| elif [[ "$SUBCOMMAND" == "weekly" ]]; then echo "$weekly_output" | |
| else echo "$session_output | $weekly_output" | |
| fi | |
| return 0 | |
| } | |
| # Determine SUBCOMMAND: if first arg doesn't start with --, it's the subcommand | |
| SUBCOMMAND="all" | |
| if [[ "${1:-}" != "" && ! "${1:-}" =~ ^-- ]]; then | |
| SUBCOMMAND="$1" | |
| shift | |
| fi | |
| FORCE_EMAIL="" | |
| BRACKET_MODE=false | |
| # Parse remaining arguments to extract --email and --bracket options | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --email) | |
| FORCE_EMAIL="$2" | |
| shift 2 | |
| ;; | |
| --bracket) | |
| BRACKET_MODE=true | |
| shift | |
| ;; | |
| *) | |
| shift | |
| ;; | |
| esac | |
| done | |
| # Get the active email from .claude.json | |
| ACTIVE_EMAIL=$(cat ~/.claude.json 2>/dev/null | jq -r '.oauthAccount.emailAddress // empty') | |
| # Use forced email if provided, otherwise use active email | |
| EMAIL="${FORCE_EMAIL:-$ACTIVE_EMAIL}" | |
| # Email subcommand doesn't need API call | |
| if [[ "$SUBCOMMAND" == "email" ]]; then | |
| if [[ "$BRACKET_MODE" == true && "$EMAIL" == "$ACTIVE_EMAIL" && -n "$EMAIL" ]]; then | |
| echo "[$EMAIL]" | |
| else | |
| echo "${EMAIL:-[No email]}" | |
| fi | |
| exit 0 | |
| fi | |
| # TTL subcommand: show time until next API refresh or error if present | |
| if [[ "$SUBCOMMAND" == "ttl" ]]; then | |
| if [[ -n "$EMAIL" && -f "$USAGES_FILE" ]]; then | |
| # Check for error first | |
| LAST_ERROR=$(jq -r --arg email "$EMAIL" '.usages[] | select(.email == $email) | .lastError // empty' "$USAGES_FILE" 2>/dev/null) | |
| if [[ -n "$LAST_ERROR" ]]; then | |
| ERROR_TYPE=$(echo "$LAST_ERROR" | jq -r '.type // "unknown"') | |
| echo "[Error: $ERROR_TYPE]" | |
| exit 0 | |
| fi | |
| LAST_ACQUIRED=$(jq -r --arg email "$EMAIL" '.usages[] | select(.email == $email) | .lastUsageAcquiredOn // empty' "$USAGES_FILE" 2>/dev/null) | |
| if [[ -n "$LAST_ACQUIRED" ]]; then | |
| LAST_EPOCH=$(iso_to_epoch "$LAST_ACQUIRED") | |
| NOW_EPOCH=$(now_epoch) | |
| AGE=$(( NOW_EPOCH - LAST_EPOCH )) | |
| TTL=$(( 180 - AGE )) | |
| [[ $TTL -lt 0 ]] && TTL=0 | |
| MINUTES=$(( TTL / 60 )) | |
| SECONDS=$(( TTL % 60 )) | |
| printf "%d:%02d\n" "$MINUTES" "$SECONDS" | |
| exit 0 | |
| fi | |
| fi | |
| echo "0:00" | |
| exit 0 | |
| fi | |
| # Check if email exists in cache | |
| EMAIL_IN_CACHE=false | |
| if [[ -n "$EMAIL" && -f "$USAGES_FILE" ]]; then | |
| CACHE_EMAIL=$(jq -r --arg email "$EMAIL" '.usages[] | select(.email == $email) | .email // empty' "$USAGES_FILE" 2>/dev/null) | |
| if [[ "$CACHE_EMAIL" == "$EMAIL" ]]; then | |
| EMAIL_IN_CACHE=true | |
| # Check if cache is fresh (< 180 seconds old) | |
| LAST_ACQUIRED=$(jq -r --arg email "$EMAIL" '.usages[] | select(.email == $email) | .lastUsageAcquiredOn' "$USAGES_FILE" 2>/dev/null) | |
| LAST_EPOCH=$(iso_to_epoch "$LAST_ACQUIRED") | |
| NOW_EPOCH=$(now_epoch) | |
| AGE=$(( NOW_EPOCH - LAST_EPOCH )) | |
| if [[ $AGE -lt 180 ]]; then | |
| output_from_usages "$EMAIL" && exit 0 | |
| fi | |
| fi | |
| fi | |
| # If email doesn't match active email, never fetch fresh data from API | |
| if [[ "$EMAIL" != "$ACTIVE_EMAIL" ]]; then | |
| echo "DEBUG: Email mismatch, not fetching API" >&2 | |
| # Try to show cache | |
| output_from_usages "$EMAIL" && exit 0 | |
| # No cache available, show ??? | |
| if [[ "$SUBCOMMAND" == "session" ]]; then echo "???" | |
| elif [[ "$SUBCOMMAND" == "weekly" ]]; then echo "???" | |
| else echo "??? | ???" | |
| fi | |
| exit 1 | |
| fi | |
| # At this point, EMAIL == ACTIVE_EMAIL, safe to proceed with API call if needed | |
| # Prevent concurrent API calls (with stale lock detection) | |
| if [[ -f "$LOCK_FILE" ]]; then | |
| # Check if lock is stale (older than 15 seconds = 3x curl timeout) | |
| LOCK_AGE=$(( $(now_epoch) - $(stat -f %m "$LOCK_FILE" 2>/dev/null || echo "0") )) | |
| if [[ $LOCK_AGE -lt 15 ]]; then | |
| output_from_usages "$EMAIL" && exit 0 | |
| echo "[Timeout]" && exit 1 | |
| fi | |
| # Stale lock detected, remove it and proceed | |
| rm -f "$LOCK_FILE" | |
| fi | |
| touch "$LOCK_FILE" | |
| trap 'rm -f "$LOCK_FILE"' EXIT | |
| TOKEN=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null | jq -r '.claudeAiOauth.accessToken // empty') | |
| if [[ -z "$TOKEN" ]]; then | |
| output_from_usages "$EMAIL" && exit 0 | |
| echo "[No credentials]" | |
| exit 1 | |
| fi | |
| RESPONSE=$(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) | |
| if [[ -z "$RESPONSE" ]]; then | |
| save_error_for_email "$EMAIL" "api_error" | |
| output_from_usages "$EMAIL" && exit 0 | |
| echo "[API Error]" | |
| exit 1 | |
| fi | |
| SESSION=$(echo "$RESPONSE" | jq -r '.five_hour.utilization // empty' 2>/dev/null) | |
| WEEKLY=$(echo "$RESPONSE" | jq -r '.seven_day.utilization // empty' 2>/dev/null) | |
| WEEKLY_RESET=$(echo "$RESPONSE" | jq -r '.seven_day.resets_at // empty' 2>/dev/null) | |
| FIVE_HOURS_RESET=$(echo "$RESPONSE" | jq -r '.five_hour.resets_at // empty' 2>/dev/null) | |
| # If API failed, use stale cache or show error | |
| if [[ -z "$SESSION" || -z "$WEEKLY" || -z "$FIVE_HOURS_RESET" || -z "$WEEKLY_RESET" ]]; then | |
| ERROR_TYPE=$(echo "$RESPONSE" | jq -r '.error.type // "parse_error"' 2>/dev/null) | |
| save_error_for_email "$EMAIL" "$ERROR_TYPE" | |
| output_from_usages "$EMAIL" && exit 0 | |
| echo "[Parse Error]" | |
| exit 1 | |
| fi | |
| SESSION_INT=${SESSION%.*} | |
| WEEKLY_INT=${WEEKLY%.*} | |
| # Save usage data per email in usages.json | |
| if [[ -n "$EMAIL" ]]; then | |
| NOW=$(date -u +"%Y-%m-%dT%H:%M:%S+00:00") | |
| USAGE_OBJ=$(jq -n \ | |
| --argjson session "$SESSION_INT" \ | |
| --arg sessionReset "$FIVE_HOURS_RESET" \ | |
| --argjson weekly "$WEEKLY_INT" \ | |
| --arg weeklyReset "$WEEKLY_RESET" \ | |
| '{sessionUsage: $session, sessionResetAt: $sessionReset, weeklyUsage: $weekly, weeklyResetAt: $weeklyReset, extraUsageEnabled: false}') | |
| mkdir -p "$(dirname "$USAGES_FILE")" | |
| if [[ -f "$USAGES_FILE" ]]; then | |
| EXISTING=$(jq -r --arg email "$EMAIL" '.usages[] | select(.email == $email) | .email // empty' "$USAGES_FILE" 2>/dev/null) | |
| if [[ -n "$EXISTING" ]]; then | |
| jq --arg email "$EMAIL" --arg now "$NOW" --argjson usage "$USAGE_OBJ" \ | |
| '(.usages[] | select(.email == $email)) |= {email: $email, lastUsageAcquiredOn: $now, usage: $usage, lastError: null}' \ | |
| "$USAGES_FILE" > "${USAGES_FILE}.tmp" && mv "${USAGES_FILE}.tmp" "$USAGES_FILE" | |
| else | |
| jq --arg email "$EMAIL" --arg now "$NOW" --argjson usage "$USAGE_OBJ" \ | |
| '.usages += [{email: $email, lastUsageAcquiredOn: $now, usage: $usage, lastError: null}]' \ | |
| "$USAGES_FILE" > "${USAGES_FILE}.tmp" && mv "${USAGES_FILE}.tmp" "$USAGES_FILE" | |
| fi | |
| else | |
| jq -n --arg email "$EMAIL" --arg now "$NOW" --argjson usage "$USAGE_OBJ" \ | |
| '{usages: [{email: $email, lastUsageAcquiredOn: $now, usage: $usage, lastError: null}]}' > "$USAGES_FILE" | |
| fi | |
| fi | |
| # Output freshly fetched data | |
| output_from_usages "$EMAIL" |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This gives following ccstatusline display :
