cd /news/developer-tools/claude-code-status-line-context-5h-7… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-57176] src=gist.github.com β†— pub= topic=developer-tools verified=true sentiment=Β· neutral

Claude Code status line: context + 5h/7d rate limit bars with reset times, clickable repo/branch/ticket/MR links (jq only; optional glab)

A developer created a custom status line for Claude Code that displays context usage, rate limit bars with reset times, and clickable links to the repository, branch, and tickets. The script uses jq and optionally glab, and supports OSC 8 hyperlinks for terminals like iTerm2 and kitty.

read11 min views1 publishedJul 13, 2026

| #!/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" |

── more in #developer-tools 4 stories Β· sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/claude-code-status-l…] indexed:0 read:11min 2026-07-13 Β· β€”