Skip to content

Instantly share code, notes, and snippets.

@rootindex
Created July 13, 2026 09:55
Show Gist options
  • Select an option

  • Save rootindex/d09e8215bef62ec9d40f394cd3f34ce1 to your computer and use it in GitHub Desktop.

Select an option

Save rootindex/d09e8215bef62ec9d40f394cd3f34ce1 to your computer and use it in GitHub Desktop.
Claude Code status line: context + 5h/7d rate limit bars with reset times, clickable repo/branch/ticket/MR links (jq only; optional glab)
#!/bin/bash
# Claude Code status line β€” two lines, subscription-focused (rate limits, no cost).
#
# ● my-project Β· ✦ Fable 5 Β· ⚑high Β· 🧠 Β· 🏷 ACME-123 (feature/ACME-123-fix) Β· πŸ”€ !42
# β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘ 62% (124k/200k) Β· 5h β–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘ 41% β†’17:00 Β· 7d β–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ 23% β†’Wed Β· ⏱ 34m
#
# Features
# - Context bar: tokens used vs window, green/yellow/red at 70/90%
# - Rate limits: 5-hour + weekly windows as matching bars, with reset time
# (clock time for 5h, weekday for 7d). Claude.ai Pro/Max only; hidden otherwise.
# - Clickable (OSC 8) project name -> repo, branch -> tree, Jira/GitLab ticket
# parsed from the branch name, open MR badge via glab (cached, non-blocking)
# - Effort + thinking badges, session duration, subdir indicator
#
# Requirements: jq. Optional: glab (MR badge), git. macOS + Linux.
# Terminals with OSC 8 support (iTerm2, Ghostty, WezTerm, kitty) get clickable
# links; others just show plain text.
#
# Install:
# 1. Save as ~/.claude/statusline.sh && chmod +x ~/.claude/statusline.sh
# 2. In ~/.claude/settings.json:
# "statusLine": { "type": "command", "command": "~/.claude/statusline.sh" }
#
# ── config ─────────────────────────────────────────────────────────────────
# Jira links are per-project: map each Jira project-key PREFIX to its base URL.
# Only prefixes listed here get a clickable link; an unknown prefix shows the
# ticket ID as plain text (never a wrong link). Add one line per project.
# GitLab/GitHub issue + repo links need NO config (built from the git origin).
jira_base_for() {
case "$1" in
# ACME) printf 'https://acme.atlassian.net' ;;
*) printf '' ;;
esac
}
GIT_CACHE_TTL=5 # seconds; branch lookup is cached per session to keep renders fast
# ───────────────────────────────────────────────────────────────────────────
input=$(cat)
# One jq call: floors numerics, defaults nulls, -1 sentinel for absent rate limit.
# 0x1F (unit separator) delimiter: non-whitespace, so `read` keeps empty fields
# instead of collapsing consecutive ones (the tab/whitespace-IFS gotcha).
IFS=$'\037' read -r MODEL PROJECT CURDIR PCT WIN TOKENS DURATION_MS EFFORT THINK SID RHOST ROWNER RNAME RATE5 RATE7 RESET5 RESET7 < <(
printf '%s' "$input" | jq -r '[
(.model.display_name // "?"),
(.workspace.project_dir // .workspace.current_dir // "."),
(.workspace.current_dir // "."),
((.context_window.used_percentage // 0) | floor),
(.context_window.context_window_size // 200000),
(.context_window.total_input_tokens // 0),
((.cost.total_duration_ms // 0) | floor),
(.effort.level? // ""),
(.thinking.enabled // false),
(.session_id // "default"),
(.workspace.repo.host? // ""),
(.workspace.repo.owner? // ""),
(.workspace.repo.name? // ""),
((.rate_limits.five_hour.used_percentage // -1) | floor),
((.rate_limits.seven_day.used_percentage // -1) | floor),
((.rate_limits.five_hour.resets_at // -1) | floor),
((.rate_limits.seven_day.resets_at // -1) | floor)
] | map(tostring) | join("")'
)
# Guard PCT so the integer comparisons below never crash on empty/non-numeric.
case "$PCT" in ''|*[!0-9]*) PCT=0 ;; esac
[ "$PCT" -gt 100 ] && PCT=100
# ── colors (literal ESC so we can printf '%s') ─────────────────────────────
ESC=$'\033'
CYAN="${ESC}[36m"; GREEN="${ESC}[32m"; YELLOW="${ESC}[33m"; RED="${ESC}[31m"
MAGENTA="${ESC}[35m"; BLUE="${ESC}[34m"; DIM="${ESC}[2m"; BOLD="${ESC}[1m"; RESET="${ESC}[0m"
BEL=$'\a'; OSC="${ESC}]8;;"
# OSC 8 hyperlink: hlink <url> <text>
hlink() { printf '%s%s%s%s%s%s' "$OSC" "$1" "$BEL" "$2" "$OSC" "$BEL"; }
# ── context bar (pure bash; tr corrupts in C locale) ───────────────────────
if [ "$PCT" -ge 90 ]; then BAR_COLOR="$RED"
elif [ "$PCT" -ge 70 ]; then BAR_COLOR="$YELLOW"
else BAR_COLOR="$GREEN"; fi
FILLED=$((PCT / 10)); EMPTY=$((10 - FILLED))
bar() { local n=$1 ch=$2 o=; while [ "$n" -gt 0 ]; do o="$o$ch"; n=$((n - 1)); done; printf '%s' "$o"; }
BAR="$(bar "$FILLED" 'β–ˆ')$(bar "$EMPTY" 'β–‘')"
fmt_k() { awk -v n="$1" 'BEGIN{if(n>=1000000)printf "%.1fM",n/1000000;else if(n>=1000)printf "%dk",n/1000;else printf "%d",n}'; }
TOK_H=$(fmt_k "$TOKENS"); WIN_H=$(fmt_k "$WIN")
MINS=$((DURATION_MS / 60000))
# ── git branch + origin remote (cached per session) ────────────────────────
mtime() { stat -f %m "$1" 2>/dev/null || stat -c %Y "$1" 2>/dev/null; }
CACHE="${TMPDIR:-/tmp}/cc_sl_branch_${SID}"
NOW=$(date +%s)
if [ -f "$CACHE" ] && [ $((NOW - $(mtime "$CACHE"))) -lt "$GIT_CACHE_TTL" ]; then
IFS=$'\037' read -r BRANCH GREMOTE < "$CACHE"
else
GITDATA=$( (cd "$CURDIR" 2>/dev/null && printf '%s\037%s' \
"$(git symbolic-ref --short -q HEAD 2>/dev/null)" \
"$(git remote get-url origin 2>/dev/null)") 2>/dev/null )
printf '%s' "$GITDATA" > "$CACHE" 2>/dev/null
IFS=$'\037' read -r BRANCH GREMOTE <<< "$GITDATA"
fi
# ── repo web URL from the git origin (handles nested GitLab groups, where
# the JSON's workspace.repo.owner/name would drop the subgroup); JSON is fallback.
REPO_URL=""
if [ -n "$GREMOTE" ]; then
REPO_URL=$(printf '%s' "$GREMOTE" | sed -E \
-e 's#^git@([^:]+):#https://\1/#' \
-e 's#^ssh://git@([^/]+)/#https://\1/#' \
-e 's#^https://[^@/]*@#https://#' \
-e 's#\.git$##')
elif [ -n "$RHOST" ]; then
REPO_URL="https://${RHOST}/${ROWNER}/${RNAME}"
fi
RWEB_HOST="${REPO_URL#https://}"; RWEB_HOST="${RWEB_HOST%%/*}"
# ── ticket from branch (Jira key, else GitLab issue number) ─────────────────
TICKET=""; TICKET_URL=""
jira_re='([A-Z]{2,}-[0-9]+)'
gl_re='^([0-9]+)[-_/]'
if [[ "$BRANCH" =~ $jira_re ]]; then
TICKET="${BASH_REMATCH[1]}"
base="$(jira_base_for "${TICKET%%-*}")" # prefix before the dash -> Jira host
[ -n "$base" ] && TICKET_URL="${base}/browse/${TICKET}"
elif [[ "$BRANCH" =~ $gl_re ]]; then
num="${BASH_REMATCH[1]}"
TICKET="#${num}"
[ -n "$REPO_URL" ] && TICKET_URL="${REPO_URL}/-/issues/${num}"
fi
# Branch name links to its page on the remote (GitLab tree, or GitHub tree).
BRANCH_DISP="$BRANCH"
if [ -n "$REPO_URL" ] && [ -n "$BRANCH" ]; then
case "$RWEB_HOST" in
*github*) TREE_URL="${REPO_URL}/tree/${BRANCH}" ;;
*) TREE_URL="${REPO_URL}/-/tree/${BRANCH}" ;;
esac
BRANCH_DISP="$(hlink "$TREE_URL" "$BRANCH")"
fi
# ── open merge request for this branch (GitLab via glab) ────────────────────
# A network call, so the result is cached (MR_TTL) and refreshed by a detached
# background job: renders never block, showing the last-known value meanwhile.
# An anti-stampede attempt file caps spawns to one per ~20s while a fetch runs.
# Needs glab authed to the origin host; if glab is missing/unauthed the MR
# segment just stays empty (no error).
MR_IID=""; MR_URL=""; MR_DRAFT=""; MR_TTL=90
if [ -n "$BRANCH" ] && [ -n "$REPO_URL" ]; then
MRCACHE="${TMPDIR:-/tmp}/cc_sl_mr_${SID}"; ATT="${MRCACHE}.attempt"
if [ -f "$MRCACHE" ]; then
IFS=$'\037' read -r cb MR_IID MR_URL MR_DRAFT < "$MRCACHE"
[ "$cb" = "$BRANCH" ] || { MR_IID=""; MR_URL=""; MR_DRAFT=""; }
fi
fresh=0
[ -f "$MRCACHE" ] && [ "$cb" = "$BRANCH" ] && [ $((NOW - $(mtime "$MRCACHE"))) -lt "$MR_TTL" ] && fresh=1
if [ "$fresh" = 0 ] && command -v glab >/dev/null 2>&1 \
&& { [ ! -f "$ATT" ] || [ $((NOW - $(mtime "$ATT"))) -ge 20 ]; }; then
: > "$ATT" 2>/dev/null
(
cd "$CURDIR" 2>/dev/null || exit
out=$(glab mr list -s "$BRANCH" -F json 2>/dev/null \
| jq -r '(.[0] // {}) | [((.iid // "")|tostring),(.web_url // ""),((.draft // false)|tostring)] | join("")' 2>/dev/null)
printf '%s\037%s\n' "$BRANCH" "$out" > "${MRCACHE}.tmp" 2>/dev/null
mv -f "${MRCACHE}.tmp" "$MRCACHE" 2>/dev/null
) >/dev/null 2>&1 &
fi
fi
MR_SEG=""
if [ -n "$MR_IID" ]; then
if [ "$MR_DRAFT" = "true" ]; then MC="$DIM"; else MC="$MAGENTA"; fi
if [ -n "$MR_URL" ]; then MR_DISP="$(hlink "$MR_URL" "!${MR_IID}")"; else MR_DISP="!${MR_IID}"; fi
MR_SEG=" Β· ${MC}πŸ”€ ${MR_DISP}${RESET}"
fi
# ── working / idle status ──────────────────────────────────────────────────
if [ -n "$BRANCH" ]; then
STATUS="${GREEN}●${RESET}"
else
STATUS="${DIM}β—‹${RESET}"
fi
# ── project name (linked to repo) + relative dir segment ───────────────────
PROJECT_NAME="${PROJECT##*/}"
if [ -n "$REPO_URL" ]; then
PROJ_DISP="$(hlink "$REPO_URL" "$PROJECT_NAME")"
else
PROJ_DISP="$PROJECT_NAME"
fi
# At root the project name is already on line 1 (status badge); only show πŸ“ once
# you've navigated into a subdir/submodule, so it reads as a "you moved" signal.
if [ "$CURDIR" = "$PROJECT" ]; then
DIRSEG=""
else
REL="${CURDIR#"$PROJECT"/}"
if [ "$REL" = "$CURDIR" ]; then DIRSEG="πŸ“ ${CURDIR##*/}"; else DIRSEG="πŸ“ β€Ί ${REL}"; fi
fi
# ── badges ─────────────────────────────────────────────────────────────────
EFFORT_BADGE=""
if [ -n "$EFFORT" ]; then
case "$EFFORT" in
low) EC="$DIM" ;; medium) EC="$GREEN" ;; high) EC="$YELLOW" ;;
xhigh|max) EC="$RED" ;; *) EC="$RESET" ;;
esac
EFFORT_BADGE=" · ${EC}⚑${EFFORT}${RESET}"
fi
THINK_BADGE=""
[ "$THINK" = "true" ] && THINK_BADGE=" · ${MAGENTA}🧠${RESET}"
# work segment: ticket+branch when working, dim "idle" otherwise
if [ -n "$BRANCH" ]; then
if [ -n "$TICKET" ]; then
if [ -n "$TICKET_URL" ]; then TKT_DISP="$(hlink "$TICKET_URL" "$TICKET")"; else TKT_DISP="$TICKET"; fi
WORK_SEG=" · 🏷 ${TKT_DISP} ${DIM}(${BRANCH_DISP})${RESET}${MR_SEG}"
else
WORK_SEG=" Β· ${DIM}(${BRANCH_DISP})${RESET}${MR_SEG}"
fi
else
WORK_SEG=" Β· ${DIM}idle${RESET}"
fi
# ── rate limits (Claude.ai Pro/Max; absent for API-key users) ──────────────
# rate bars: same glyphs/colors/width as the context bar
rate_bar() {
local p=$1 c
if [ "$p" -ge 90 ]; then c="$RED"
elif [ "$p" -ge 70 ]; then c="$YELLOW"
else c="$GREEN"; fi
local f=$((p / 10)); [ "$f" -gt 10 ] && f=10
printf '%s%s%s%s' "$c" "$(bar "$f" 'β–ˆ')" "$(bar $((10 - f)) 'β–‘')" "$RESET"
}
# fmt_epoch <epoch> <format>: BSD date first, GNU date fallback
fmt_epoch() { date -r "$1" "$2" 2>/dev/null || date -d "@$1" "$2" 2>/dev/null; }
# reset markers: 5h window resets within hours (clock time), 7d within days (weekday)
R5=""; R7=""
[ "$RESET5" -gt 0 ] 2>/dev/null && R5=" ${DIM}β†’$(fmt_epoch "$RESET5" +%H:%M)${RESET}"
[ "$RESET7" -gt 0 ] 2>/dev/null && R7=" ${DIM}β†’$(fmt_epoch "$RESET7" +%a)${RESET}"
RATE_SEG=""
[ "$RATE5" -ge 0 ] 2>/dev/null && RATE_SEG=" Β· 5h $(rate_bar "$RATE5") ${RATE5}%${R5}"
[ "$RATE7" -ge 0 ] 2>/dev/null && RATE_SEG="${RATE_SEG} Β· 7d $(rate_bar "$RATE7") ${DIM}${RATE7}%${RESET}${R7}"
# ── render ─────────────────────────────────────────────────────────────────
printf '%s %s · ✦ %s%s%s%s%s\n' \
"$STATUS" "$PROJ_DISP" "${CYAN}${MODEL}${RESET}" "$EFFORT_BADGE" "$THINK_BADGE" "$WORK_SEG" ""
DIR_PREFIX=""; [ -n "$DIRSEG" ] && DIR_PREFIX="${DIRSEG} Β· "
printf '%s%s%s%s %s%% %s(%s/%s)%s%s · ⏱ %sm\n' \
"$DIR_PREFIX" "$BAR_COLOR" "$BAR" "$RESET" "$PCT" "$DIM" "$TOK_H" "$WIN_H" "$RESET" "$RATE_SEG" "$MINS"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment