Skip to content

Instantly share code, notes, and snippets.

@ryangurn
Created June 6, 2026 06:29
Show Gist options
  • Select an option

  • Save ryangurn/0bcd0709e5af268a41f005daf02da101 to your computer and use it in GitHub Desktop.

Select an option

Save ryangurn/0bcd0709e5af268a41f005daf02da101 to your computer and use it in GitHub Desktop.
Claude Code status line — 2-line, color+emoji, shift-clickable links (repo/branch/PR/cloud), context gauge, cost, and an agent-writable note. See README.md.

Claude Code Status Line

A two-line, color + emoji status line for Claude Code, with shift-clickable hyperlinks (repo, branch, PR, cloud subscription), a live context-window gauge, session cost/duration, and an agent-writable note that the model can update to keep you posted on long-running work.

🕐 11:06P/1:06A/2:06A  🖥️ my-repo  (✨ my-feature-branch)  🟢 dev  ☁ my-subscription  🐳 3  ⇄ 8
🔮 [Opus 4.8]  ⚙ medium  🤖 1  🧠 764k 77%  +176/-3  ⏱W 1d 10h ⚡A 3h50m  💲12.40                    📌 🚀 deploying
  • Top line = environment / where you are: clock (multi-zone), directory, git branch + dirty/ahead/behind, prod-vs-dev flag, cloud subscription, terraform workspace, php, docker, MCP server count.
  • Bottom line = this Claude session: model, effort, subagent count, context-window usage, lines changed, wall/active time, cost — plus a right-aligned 📌 note.

Every clickable segment uses OSC 8 hyperlinks: shift-click (or ⌘/ctrl-click, depending on terminal) opens the target in your browser/Finder.

Requirements

Tool For Required?
jq parsing Claude Code's status JSON yes
git branch / dirty / ahead-behind / repo URL recommended
gh GitHub PR detection optional
az Azure subscription + Azure DevOps PR detection optional
docker running-container count optional
php PHP version in PHP projects optional
python3 right-aligning the 📌 note (graceful fallback without it) optional
A terminal with OSC 8 support clickable links recommended (Ghostty, iTerm2, WezTerm, kitty, …)

Every optional tool degrades gracefully — missing tool just means that segment is hidden.

Install

  1. Copy the script into your Claude config dir and make it executable:
    cp statusline.sh ~/.claude/statusline.sh
    chmod +x ~/.claude/statusline.sh
  2. Wire it into ~/.claude/settings.json (see settings.example.json — merge the statusLine block):
    {
      "statusLine": { "type": "command", "command": "~/.claude/statusline.sh", "padding": 0 }
    }
  3. (Optional) Install the status-note skill so Claude can post 📌 notes when you say "keep me posted":
    cp -r skills/status-note ~/.claude/skills/status-note
  4. Start (or restart) Claude Code. The status line appears at the bottom.

The 📌 note (agent-writable, per-session)

Claude — or you — can write a short status to a per-session scratch file; it renders right-aligned at the end of the session line. With the skill installed, just tell Claude "keep me posted" and it maintains the note as work progresses.

# plain note (persists until changed/cleared):
echo "0 ⏳ applying migration" > ~/.claude/.cache/statusline_note_$CLAUDE_CODE_SESSION_ID

# clickable note — the whole 📌 links to a URL (shift-click):
echo "0 https://example.com/ci/runs/123 🚀 deploying" > ~/.claude/.cache/statusline_note_$CLAUDE_CODE_SESSION_ID

# auto-expiring note (hides after 30 min):
echo "$(($(date +%s)+1800)) ⚠️ tests failing" > ~/.claude/.cache/statusline_note_$CLAUDE_CODE_SESSION_ID

# clear:
rm -f ~/.claude/.cache/statusline_note_$CLAUDE_CODE_SESSION_ID

Format: <expiry_epoch> [url] <text>. expiry 0 = persist; a future epoch auto-hides (and self-deletes) the note. The file is suffixed with the session id, so notes never leak between open Claude windows.

Customize

Search the script for CUSTOMIZE — every extension point is tagged. Highlights:

  • Directory icons (📁 directory block): the case "$leaf" ladder maps repo-name families → icons (Work.* → 💼, platform-* → 🏗️, *mcp* → 🔌, …), then falls back to language (artisan → 🐘, go.mod → 🐹, package.json → 📦) and host (🔷 ADO / 🐙 GitHub). Edit the patterns to match your repos.
  • Branch-type icons: feature/ → ✨, bugfix/ → 🐛, release/ → 🚀, etc. Add your own prefixes.
  • Model icons: 🔮 Opus / ✍️ Sonnet / 🍃 Haiku — add cases as needed.
  • Clock timezones: the for tz in … loop. Use any tz-database names; add/remove zones freely.
  • Prod/dev flag: the case "$lc" on the subscription name. Tune patterns so prod goes red.
  • Context thresholds: the 60 / 85 percentages controlling green→yellow→red.

How it reads data

  • Claude Code pipes a JSON object on stdin each render (model, cwd, cost, session id, context_window, transcript path, …). The script parses it in one jq pass.
  • Terminal width for right-aligning the note comes from the COLUMNS env var, which Claude Code sets when invoking the script (it's 0 in a normal interactive shell — that's expected).
  • Caching: network/slow lookups are cached under ~/.claude/.cache/ — Azure sub & docker count for 45s, PR status for 90s, PHP version once. Keeps every render fast.

Notes & gotchas

  • jq fields are joined with the \x1f unit-separator (not tab) so empty fields don't collapse when read back with IFS.
  • Subagent tokens are not counted in the 🧠 context gauge (subagents have their own context), but they do bill to 💲 cost — so a parallel agent swarm shows cost rising while 🧠 stays flat.
  • memory-note.example.md + MEMORY.snippet.md are optional — only useful if you use Claude Code's memory feature and want future sessions to understand the setup.

Files

File What it is
statusline.sh the status line script (the only required file)
settings.example.json the settings.json keys to merge in
skills/status-note/SKILL.md optional skill: lets Claude post 📌 notes on "keep me posted"
memory-note.example.md optional Claude-memory doc describing the setup
MEMORY.snippet.md the one-line index entry for the memory doc
name statusline-setup
description Custom Claude Code status line script — segments, caching, where it lives
metadata
type
reference

NOTE: This is an OPTIONAL Claude Code "memory" file (drop it in your project's memory dir and index it in MEMORY.md). It just documents the setup so future sessions understand it. Skip if you don't use the memory feature.

Status line is a script at ~/.claude/statusline.sh, wired via statusLine.command in ~/.claude/settings.json (user-level → applies to every project). TWO LINES: TOP = environment/aux, BOTTOM = session. (Emitted bottom-then-top so the env line prints first.)

Hyperlinks: terminal must support OSC8 (Ghostty/iTerm2/WezTerm/kitty). dir + branch + PR + az-sub are shift-clickable via a link() helper (\033]8;;URL\033\\TEXT\033]8;;\033\\).

  • Dir click → repo web page (repo_web_url() converts the origin remote: github.com incl. ssh:// and git@github-ALIAS: host-alias forms; dev.azure.com; AND legacy visualstudio.com / vs-ssh), or Finder file:// if no remote.
  • Branch click → branch page (?version=GB<br> for ADO, /tree/<br> for GitHub).
  • Az sub name → Portal overview https://portal.azure.com/#@<tenantId>/resource/subscriptions/<subId>/overview.

Dir icon cascade (name-family > language > host); leaf folder NEVER abbreviated. Branch shortening: icon REPLACES the text prefix (✨ feature · 🐛 bugfix · 🚀 release · 🔥 hotfix · 🧹 chore · 🌳 trunk · 🌿 other), drops a leading first.last/ segment, middle-truncates past 30 chars. Detached HEAD = ⚠️+sha.

TOP (env): 🕐 clock (3 zones, leftmost) · 📁 dir · git (<icon> branch ●dirty ↑ahead ↓behind) · env-flag (🟢 dev / 🔴 PROD / 🟡 unknown, from az sub name) · ☁ az sub · 🧊 tf workspace (reads .terraform/environment) · 🐘 php · 🐳 docker · ⇄ MCP count.

BOTTOM (session): [model] (🔮 Opus / ✍️ Sonnet / 🍃 Haiku / 🤖 other) · 📌 note · ⚙ effort · 🤖 subagent count · 🧠 context-window (prefers JSON context_window.used_percentage, falls back to transcript usage) · +added/-removed · ⏱W wall + ⚡A api duration · 💲 cost.

📌 note: per-session scratch file ~/.claude/.cache/statusline_note_$CLAUDE_CODE_SESSION_ID, format <expiry> [url] <text> (expiry 0 = persist; future epoch = auto-expire). STRICT per-session — no global fallback (prevents cross-session bleed). Right-aligned at end of session line (needs COLUMNS + python3).

Caching in ~/.claude/.cache/: az_sub + docker_n refresh every 45s; php_ver cached once; pr_* cached 90s. Conditional segments (tf/php/docker/pr/note) only render when relevant.

Gotchas: jq fields joined with \x1f (not tab) + IFS=$'\x1f' so empty fields don't collapse. COLUMNS is 0 in a normal shell but set correctly when Claude Code invokes the script. Subagent tokens are NOT in 🧠 (separate context) but DO bill to 💲 cost.

{
"//": "Minimal example — merge these keys into your ~/.claude/settings.json.",
"//statusLine": "This is the only block REQUIRED to use the status line script.",
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh",
"padding": 0
},
"//includeCoAuthoredBy": "Optional: set false to strip the Co-Authored-By / 'Generated with Claude Code' trailers from commits & PRs.",
"includeCoAuthoredBy": false,
"//effortLevel": "Optional: the status line reads this as a fallback when effort isn't in the live status JSON.",
"effortLevel": "medium"
}
name status-note
description Keep the user posted on live progress by writing short status notes to the Claude Code status line as a task advances. Use when the user says "keep me up to date", "keep me posted", "provide me updates", "give me updates", "let me know where we are", "show progress", "status updates as you go", or otherwise asks for an ongoing/current sense of where things are in a multi-step process. Once active, update the note at each meaningful phase transition and clear it when done. Not for one-off questions or when the user wants a written summary in chat.
version 1.0.0
tags capability:progress, surface:statusline

Status Note

Surface live, at-a-glance progress on the Claude Code status line (bottom of the terminal) so the user always knows where we are in a longer process — without scrolling chat.

Mechanism

The status line script renders a bold-yellow 📌 segment, right-aligned at the end of the session line, from a per-session scratch file:

~/.claude/.cache/statusline_note_$CLAUDE_CODE_SESSION_ID

ALWAYS write to the per-session path so the note only shows in THIS session, not every open Claude window. $CLAUDE_CODE_SESSION_ID is set in the Bash environment — use it directly.

File format is a single line, either:

  • <expiry_epoch> <text> — plain note
  • <expiry_epoch> <url> <text> — note whose entire 📌 becomes shift-clickable, opening <url>

Fields:

  • <expiry_epoch> = Unix epoch seconds after which the note auto-hides (script self-deletes it). Use 0 to persist until you change/clear it.
  • <url> = optional; must start with http:// or https:// as the 2nd whitespace-delimited token. Link it to whatever the status is about — a CI run, a PR, a cloud console page, a dashboard.
  • <text> = the short status, lead with an emoji for scannability.

How to use it

Write / update a plain note (Bash tool):

echo "0 ⏳ step 2/5 — applying migration" > ~/.claude/.cache/statusline_note_$CLAUDE_CODE_SESSION_ID

Write a clickable note (links the 📌 to e.g. the live CI run):

echo "0 https://example.com/ci/runs/12345 🚀 deploying" \
  > ~/.claude/.cache/statusline_note_$CLAUDE_CODE_SESSION_ID

Good link targets: a CI/CD run, the PR page, a cloud-console resource, a metrics dashboard — whatever the user would want to jump to for this status.

Auto-expiring note (transient warning that fades in 30 min):

echo "$(($(date +%s)+1800)) ⚠️ 3 tests still failing" > ~/.claude/.cache/statusline_note_$CLAUDE_CODE_SESSION_ID

Clear it when the work is finished:

rm -f ~/.claude/.cache/statusline_note_$CLAUDE_CODE_SESSION_ID

Behavior once active

When the user opts in (any of the trigger phrases), treat status-noting as a standing instruction for the current task:

  1. Set an initial note as soon as you understand the plan, e.g. 📋 starting: <task> (0/N).
  2. Update at each meaningful phase transition — not every tool call, but each real step the user would care about. Prefer a step counter when the work is enumerable: ⏳ 3/6 — <what's happening now>.
  3. Reflect state with a leading emoji so it reads at a glance:
    • ⏳ in progress · 🔨 building · 🧪 testing · 🚀 deploying · 🔍 investigating · ⏸️ waiting/blocked · ⚠️ attention needed · ✅ step done
  4. Keep it short — it shares one terminal line. Aim for under ~40 chars; lead with the emoji and the most identifying detail.
  5. Attach a link when there's an obvious target. If the status refers to something with a URL (a run you just queued, a PR you opened, a console resource, a dashboard), use the <url> field so the 📌 is shift-clickable. Prefer the most actionable destination for that status.
  6. Clear the note (rm -f) when the overall task completes, or replace it with a brief ✅ done — <task> and let it auto-expire (set expiry ~10 min out).
  7. Keep notes persistent (0) while actively working; use a future epoch only for things that should self-clear if you go quiet.

Notes

  • The note is purely a UI nicety on the status line; it does not replace explaining progress in chat when that's warranted.
  • One note at a time — writing overwrites the previous. There is no queue.
  • Notes are per-session (file is suffixed with $CLAUDE_CODE_SESSION_ID); a note set here never leaks into other open Claude sessions.
  • If the user says "stop the updates" / "you can stop" / similar, clear the file and stop maintaining it.
#!/bin/bash
# Claude Code status line (two lines).
# TOP (env/aux): 🕐 clock · 📁 dir · git · env-flag · ☁ az · 🧊 tf · 🐘 php · 🐳 docker · ⇄ mcp
# BOT (session): [model] · 📌 note · ⚙ effort · 🤖 subagents · 🧠 ctx · +/- lines · ⏱ time · 💲 cost
#
# Requires: jq. Optional: git, gh, az, docker, php, python3 (python3 only for right-aligning the note).
# Terminal hyperlinks (shift-click) use OSC8 — works in Ghostty, iTerm2, WezTerm, kitty, etc.
input=$(cat)
# ---- colors ----
RST=$'\033[0m'; DIM=$'\033[90m'; BOLD=$'\033[1m'
YEL=$'\033[33m'; MAG=$'\033[35m'; GRN=$'\033[32m'; RED=$'\033[31m'
BLU=$'\033[34m'; CYN=$'\033[36m'
OSC=$'\033]8;;'; ST=$'\033\\' # OSC8 hyperlink: ${OSC}URL${ST}TEXT${OSC}${ST}
# link <url> <text> -> emits a shift-clickable hyperlink (OSC8)
link() { printf '%s%s%s%s%s%s' "$OSC" "$1" "$ST" "$2" "$OSC" "$ST"; }
# repo_web_url <dir> -> browsable https URL for the origin remote (GitHub or Azure DevOps), else ""
repo_web_url() {
local r; r=$(git -C "$1" remote get-url origin 2>/dev/null) || return
case "$r" in
git@github.com:*) r="https://github.com/${r#git@github.com:}"; echo "${r%.git}" ;;
ssh://git@github.com/*) r="https://github.com/${r#ssh://git@github.com/}"; echo "${r%.git}" ;;
git@github-*:*) r="https://github.com/${r#git@github-*:}"; echo "${r%.git}" ;; # ssh host-alias (2nd account)
https://github.com/*) echo "${r%.git}" ;;
# ADO ssh: git@ssh.dev.azure.com:v3/ORG/PROJECT/REPO -> https://dev.azure.com/ORG/PROJECT/_git/REPO
git@ssh.dev.azure.com:v3/*) local p="${r#git@ssh.dev.azure.com:v3/}"; local o="${p%%/*}"; local rest="${p#*/}"; local pr="${rest%%/*}"; local rp="${rest#*/}"; echo "https://dev.azure.com/${o}/${pr}/_git/${rp}" ;;
# ADO https: https://ORG@dev.azure.com/ORG/PROJECT/_git/REPO -> strip creds
https://*@dev.azure.com/*) echo "https://dev.azure.com/${r#*@dev.azure.com/}" ;;
https://dev.azure.com/*) echo "$r" ;;
# legacy visualstudio.com (https already browsable; strip embedded creds if any)
https://*@*.visualstudio.com/*) local h="${r#https://*@}"; echo "https://${h}" ;;
https://*.visualstudio.com/*) echo "$r" ;;
# legacy vs-ssh: USER@vs-ssh.visualstudio.com:v3/ORG/PROJECT/REPO
*@vs-ssh.visualstudio.com:v3/*) local p="${r#*vs-ssh.visualstudio.com:v3/}"; local o="${p%%/*}"; local rest="${p#*/}"; local pr="${rest%%/*}"; local rp="${rest#*/}"; echo "https://${o}.visualstudio.com/${pr}/_git/${rp}" ;;
*) echo "" ;;
esac
}
# pr_info <dir> <branch> <weburl> -> "icon<TAB>#number<TAB>prurl" for current branch's PR, else ""
# cached 90s per repo+branch (network calls). Icon by state: 🟢 open · 📝 draft · 🟣 merged · 🔴 closed
pr_info() {
local d="$1" br="$2" web="$3"
[ -n "$br" ] || return
local key; key=$(echo "${d}@${br}" | tr -c 'A-Za-z0-9' '_')
local pf="$cache/pr_${key}"
if [ -z "$(find "$pf" -mtime -90s 2>/dev/null)" ]; then
local line=""
case "$web" in
*github.com*)
line=$( cd "$d" 2>/dev/null && gh pr status --json number,state,isDraft,url -q \
'.currentBranch | select(.number) | "\(.number)\t\(.state)\t\(.isDraft)\t\(.url)"' 2>/dev/null) ;;
*dev.azure.com*|*visualstudio.com*)
# web = https://ORG.visualstudio.com/PROJECT/_git/REPO OR https://dev.azure.com/ORG/PROJECT/_git/REPO
local org proj repo
repo="${web##*/_git/}"; proj="${web%/_git/*}"; proj="${proj##*/}"
case "$web" in
*dev.azure.com*) org="https://dev.azure.com/$(echo "${web#https://dev.azure.com/}" | cut -d/ -f1)" ;;
*) org="$(echo "$web" | sed -E 's#(https://[^/]+).*#\1#')" ;;
esac
line=$(az repos pr list --org "$org" --project "$proj" --repository "$repo" \
--source-branch "$br" --status all --top 1 \
--query "[0].{n:pullRequestId,s:status,d:isDraft}" -o tsv 2>/dev/null \
| awk -F'\t' 'NF{print $1"\t"$2"\t"$3"\t""'"${web}"'/pullrequest/"$1}') ;;
esac
printf '%s' "$line" >"$pf"
fi
local n st draft url; IFS=$'\t' read -r n st draft url <"$pf" 2>/dev/null
[ -n "$n" ] || return
local ic
case "$st" in
OPEN|active) ic="🟢" ;;
MERGED|completed) ic="🟣" ;;
CLOSED|abandoned) ic="🔴" ;;
*) ic="🔵" ;;
esac
case "$draft" in true|True) ic="📝" ;; esac
printf '%s\t#%s\t%s' "$ic" "$n" "$url"
}
# ---- pull fields from JSON (single jq pass; \x1f-joined so empty fields don't collapse) ----
IFS=$'\x1f' read -r dir model tpath dur_ms api_ms added removed cost sid cols ctxtok ctxpct < <(
echo "$input" | jq -j '[
.workspace.current_dir // .cwd // "",
.model.display_name // "?",
.transcript_path // "",
(.cost.total_duration_ms // 0 | tostring),
(.cost.total_api_duration_ms // 0 | tostring),
(.cost.total_lines_added // 0 | tostring),
(.cost.total_lines_removed // 0 | tostring),
(.cost.total_cost_usd // 0 | tostring),
(.session_id // ""),
((.width // .cols // .columns // .terminal.width // .terminal.cols // 0) | tostring),
((.context_window.total_input_tokens // 0) | tostring),
((.context_window.used_percentage // 0) | tostring)
] | join("")'
)
cache="$HOME/.claude/.cache"; mkdir -p "$cache"
top="" # session info
bot="" # environment / aux
# =====================================================================
# TOP LINE (rendered second) — session
# =====================================================================
# ---- [model] (icon by family) ----
# CUSTOMIZE: model icons. Add cases for other models you use.
case "$model" in
*Opus*|*opus*) micon="🔮" ;;
*Sonnet*|*sonnet*) micon="✍️" ;;
*Haiku*|*haiku*) micon="🍃" ;;
*) micon="🤖" ;;
esac
top+=" ${DIM}${micon} [${model}]${RST}"
# ---- 📌 model/agent-driven note (per-session scratch file) ----
# Format: "<expiry_epoch> <text>" OR "<expiry_epoch> <url> <text>" (url makes 📌 shift-clickable)
# echo "0 ⏳ deploying" > ~/.claude/.cache/statusline_note_$CLAUDE_CODE_SESSION_ID
# echo "0 https://example.com/run/123 🚀 build" > ...same file
# expiry 0 = persist until changed/cleared; future epoch = auto-hide + self-delete.
note=""
notefile="$cache/statusline_note_$sid" # STRICT per-session: no global fallback (avoids cross-session bleed)
if [ -n "$sid" ] && [ -f "$notefile" ]; then
read -r nexp nrest < "$notefile" 2>/dev/null
now=$(date +%s)
if [ -n "$nrest" ] && { [ -z "$nexp" ] || [ "$nexp" = "0" ] || { [ "$nexp" -gt "$now" ] 2>/dev/null; }; }; then
nurl=""; ntext="$nrest"
case "$nrest" in
http://*|https://*) nurl="${nrest%% *}"; ntext="${nrest#* }" ;;
esac
if [ -n "$nurl" ] && [ "$nurl" != "$ntext" ]; then
note="${BOLD}${YEL}$(link "$nurl" "📌 ${ntext}")${RST}"
else
note="${BOLD}${YEL}📌 ${ntext}${RST}"
fi
else
rm -f "$notefile" # expired -> clean up
fi
fi
# ---- ⚙ effort level (from status JSON, else settings.json) ----
eff=$(echo "$input" | jq -r '.effort.level // .effortLevel // empty' 2>/dev/null)
[ -z "$eff" ] && eff=$(jq -r '.effortLevel // empty' "$HOME/.claude/settings.json" 2>/dev/null)
if [ -n "$eff" ]; then
case "$eff" in
high|max) ec="$RED" ;; medium) ec="$YEL" ;; *) ec="$GRN" ;;
esac
top+=" ${ec}⚙ ${eff}${RST}"
fi
# ---- 🤖 subagents spawned this session (count of agent-*.jsonl; instant) ----
# Subagents run in their own context (NOT counted in 🧠) but DO bill to 💲 cost.
if [ -n "$tpath" ]; then
sadir="${tpath%.jsonl}/subagents"
if [ -d "$sadir" ]; then
nsa=$(ls "$sadir"/agent-*.jsonl 2>/dev/null | grep -c .)
[ "$nsa" -gt 0 ] 2>/dev/null && top+=" ${MAG}🤖 ${nsa}${RST}"
fi
fi
# CUSTOMIZE: context % thresholds (60/85) and color live in the block just below.
# ---- 🧠 context window usage (prefer JSON context_window; fall back to transcript) ----
ctx=0
[ -n "$ctxtok" ] && [ "$ctxtok" -gt 0 ] 2>/dev/null && ctx="$ctxtok"
if [ "$ctx" = "0" ] && [ -n "$tpath" ] && [ -f "$tpath" ]; then
ctx=$(grep '"usage"' "$tpath" 2>/dev/null | tail -1 | jq -r '
(.message.usage // {}) |
((.input_tokens // 0) + (.cache_read_input_tokens // 0) + (.cache_creation_input_tokens // 0))
' 2>/dev/null)
fi
if [ -n "$ctx" ] && [ "$ctx" -gt 0 ] 2>/dev/null; then
k=$((ctx / 1000))
# color by exact % when provided, else by token thresholds (scaled up for 1M-context models)
if [ -n "$ctxpct" ] && [ "$ctxpct" -gt 0 ] 2>/dev/null; then
if [ "$ctxpct" -ge 85 ]; then c="$RED"
elif [ "$ctxpct" -ge 60 ]; then c="$YEL"
else c="$GRN"; fi
top+=" ${c}🧠 ${k}k ${ctxpct}%${RST}"
else
case "$model" in *1[Mm]*|*"[1m]"*) hi=800; mid=500 ;; *) hi=160; mid=120 ;; esac
if [ "$k" -ge "$hi" ]; then c="$RED"
elif [ "$k" -ge "$mid" ]; then c="$YEL"
else c="$GRN"; fi
top+=" ${c}🧠 ${k}k${RST}"
fi
fi
# fmt_dur <ms> -> "1d 4h" / "3h12m" / "48m" / "30s"
fmt_dur() {
local s=$(( $1 / 1000 )) dd hh mm
dd=$(( s / 86400 )); hh=$(( (s % 86400) / 3600 )); mm=$(( (s % 3600) / 60 ))
if [ "$dd" -gt 0 ]; then printf '%dd %dh' "$dd" "$hh"
elif [ "$hh" -gt 0 ]; then printf '%dh%dm' "$hh" "$mm"
elif [ "$mm" -gt 0 ]; then printf '%dm' "$mm"
else printf '%ds' "$s"; fi
}
# ---- +added / -removed lines ----
if [ "$added" -gt 0 ] 2>/dev/null || [ "$removed" -gt 0 ] 2>/dev/null; then
top+=" ${GRN}+${added}${RST}${DIM}/${RST}${RED}-${removed}${RST}"
fi
# ---- ⏱ duration (W=wall, A=api/active) and 💲 cost, grouped at end of session line ----
if [ "$dur_ms" -gt 0 ] 2>/dev/null; then
top+=" ${DIM}⏱W $(fmt_dur "$dur_ms")${RST}"
[ "$api_ms" -gt 0 ] 2>/dev/null && top+=" ${DIM}⚡A $(fmt_dur "$api_ms")${RST}"
fi
if [ "$(echo "$cost > 0" | bc -l 2>/dev/null)" = "1" ]; then
top+=" ${DIM}💲$(printf '%.2f' "$cost")${RST}"
fi
# =====================================================================
# BOTTOM LINE (rendered first) — environment / aux
# =====================================================================
# ---- 🕐 clock: 12h A/P, three zones slashed PST/CST/EST, times only ----
# CUSTOMIZE: change/add/remove timezones here (any tz database name). One time per zone.
clk=""
for tz in America/Los_Angeles America/Chicago America/New_York; do
t=$(TZ="$tz" date '+%-I:%M%p') # e.g. 11:05PM
ap="${t: -2}"; t="${t%??}" # ap=PM, t=11:05
clk+="${t}${ap:0:1}/" # 11:05P/
done
bot+=" ${DIM}🕐 ${clk%/}${RST}"
# ---- 📁 directory: smart icon (name family > language > host > location); full leaf ----
# shift-click opens the repo's web page (GitHub/ADO), or the folder in Finder if not a repo.
# CUSTOMIZE the name-family cases below for your own repo collection.
leaf="${dir##*/}"
dweb=$(repo_web_url "$dir")
case "$dir" in
"$HOME") dicon="🏠"; leaf="~" ;;
"$HOME"/Desktop/Github/*)
# inside the repo collection — pick by name family, then language, then host
case "$leaf" in
Work.*|*-work) dicon="💼" ;; # e.g. work / org repos
platform-*) dicon="🏗️" ;; # e.g. a platform family
*mcp*|*MCP*) dicon="🔌" ;; # MCP servers
*deploy*|*-iac|*iac-*) dicon="🧱" ;; # deploy / IaC
*.com) dicon="🌐" ;; # personal site
*)
if [ -f "$dir/artisan" ]; then dicon="🐘" # Laravel
elif [ -f "$dir/go.mod" ]; then dicon="🐹" # Go
elif [ -f "$dir/package.json" ]; then dicon="📦" # Node
elif [ -n "$dweb" ]; then
case "$dweb" in *dev.azure.com*|*visualstudio.com*) dicon="🔷" ;; *) dicon="🐙" ;; esac
else dicon="📂"; fi ;;
esac ;;
"$HOME"/Desktop/*) dicon="🖥️" ;;
"$HOME"/*) dicon="🏠" ;;
*) dicon="📁" ;;
esac
dtarget="${dweb:-file://$dir}"
bot+=" ${YEL}${dicon} $(link "$dtarget" "$leaf")${RST}"
# ---- git: <icon> branch ●dirty ↑ahead ↓behind ----
if branch=$(git -C "$dir" symbolic-ref --short HEAD 2>/dev/null); then
# type icon REPLACES the text prefix
short_branch="$branch"
case "$branch" in
main|master) bicon="🌳" ;;
feature/*) bicon="✨"; short_branch="${branch#feature/}" ;; # CUSTOMIZE: branch-type icons
bugfix/*) bicon="🐛"; short_branch="${branch#bugfix/}" ;;
hotfix/*) bicon="🔥"; short_branch="${branch#hotfix/}" ;;
release/*) bicon="🚀"; short_branch="${branch#release/}" ;;
chore/*) bicon="🧹"; short_branch="${branch#chore/}" ;;
*) bicon="🌿" ;;
esac
# drop a leading "first.last/" user-name segment -> keeps the meaningful tail
short_branch=$(echo "$short_branch" | sed -E 's#^[a-z]+\.[a-z]+/##')
# middle-truncate if too long (keeps BOTH ends identifiable)
if [ "${#short_branch}" -gt 30 ]; then
short_branch="${short_branch:0:14}…${short_branch: -14}"
fi
# branch shift-clickable -> branch web page (GitHub /tree/, ADO ?version=GB)
if [ -n "$dweb" ]; then
case "$dweb" in
*dev.azure.com*|*visualstudio.com*) burl="${dweb}?version=GB${branch}" ;;
*) burl="${dweb}/tree/${branch}" ;;
esac
blabel=$(link "$burl" "$short_branch")
else
blabel="$short_branch"
fi
git_str="${bicon} ${blabel}"
dirty=$(git -C "$dir" status --porcelain --no-optional-locks 2>/dev/null | grep -c .)
[ "$dirty" -gt 0 ] && git_str+=" ●${dirty}"
upstream=$(git -C "$dir" rev-list --left-right --count @{u}...HEAD 2>/dev/null)
if [ -n "$upstream" ]; then
behind=$(echo "$upstream" | cut -f1); ahead=$(echo "$upstream" | cut -f2)
[ "$ahead" -gt 0 ] 2>/dev/null && git_str+=" ↑${ahead}"
[ "$behind" -gt 0 ] 2>/dev/null && git_str+=" ↓${behind}"
fi
bot+=" ${MAG}(${git_str})${RST}"
elif sha=$(git -C "$dir" rev-parse --short HEAD 2>/dev/null); then
bot+=" ${MAG}(⚠️ ${sha})${RST}" # detached HEAD
fi
# ---- PR for current branch (GitHub + Azure DevOps, cached); shift-click -> PR page ----
if [ -n "$branch" ] && [ -n "$dweb" ]; then
IFS=$'\t' read -r pric prnum prurl < <(pr_info "$dir" "$branch" "$dweb")
if [ -n "$prnum" ]; then
if [ -n "$prurl" ]; then
bot+=" ${CYN}${pric} $(link "$prurl" "PR ${prnum}")${RST}"
else
bot+=" ${CYN}${pric} PR ${prnum}${RST}"
fi
fi
fi
# ---- ☁ azure subscription (cached 45s) + env flag derived from its name ----
# name shift-clickable -> Portal subscription overview. Requires the `az` CLI + a login.
subfile="$cache/az_sub"
if [ -z "$(find "$subfile" -mtime -45s 2>/dev/null)" ]; then
az account show --query '[name, id, tenantId]' -o tsv 2>/dev/null | tr '\n' '\t' >"$subfile" || : >"$subfile"
fi
IFS=$'\t' read -r sub subid subtenant _ < "$subfile" 2>/dev/null
if [ -n "$sub" ]; then
lc=$(echo "$sub" | tr '[:upper:]' '[:lower:]')
# CUSTOMIZE: env detection — match your subscription naming to color prod red vs dev green.
case "$lc" in
*prod*|*prd*) bot+=" ${BOLD}${RED}🔴 PROD${RST}" ;;
*dev*|*test*|*sandbox*|*nonprod*) bot+=" ${GRN}🟢 dev${RST}" ;;
*) bot+=" ${YEL}🟡 ?${RST}" ;;
esac
if [ -n "$subid" ] && [ -n "$subtenant" ]; then
suburl="https://portal.azure.com/#@${subtenant}/resource/subscriptions/${subid}/overview"
bot+=" ${BLU}☁ $(link "$suburl" "$sub")${RST}"
else
bot+=" ${BLU}☁ ${sub}${RST}"
fi
fi
# ---- 🧊 terraform workspace (instant: read .terraform/environment) ----
if [ -f "$dir/.terraform/environment" ]; then
ws=$(cat "$dir/.terraform/environment" 2>/dev/null)
[ -n "$ws" ] && bot+=" ${CYN}🧊 ${ws}${RST}"
fi
# ---- 🐘 php version (only inside a PHP/Laravel project; cached once) ----
if [ -f "$dir/composer.json" ] || [ -f "$dir/artisan" ] || ls "$dir"/*.php >/dev/null 2>&1; then
pvfile="$cache/php_ver"
[ -f "$pvfile" ] || php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;' >"$pvfile" 2>/dev/null
pv=$(cat "$pvfile" 2>/dev/null)
[ -n "$pv" ] && bot+=" ${MAG}🐘 ${pv}${RST}"
fi
# ---- 🐳 docker running container count (cached 45s) ----
dfile="$cache/docker_n"
if [ -z "$(find "$dfile" -mtime -45s 2>/dev/null)" ]; then
docker ps -q 2>/dev/null | grep -c . >"$dfile" 2>/dev/null || echo 0 >"$dfile"
fi
dn=$(cat "$dfile" 2>/dev/null)
[ -n "$dn" ] && [ "$dn" -gt 0 ] 2>/dev/null && bot+=" ${BLU}🐳 ${dn}${RST}"
# ---- ⇄ MCP server count (global + this project, from ~/.claude.json) ----
cfg="$HOME/.claude.json"
if [ -f "$cfg" ]; then
mcpn=$(jq --arg d "$dir" -r '
((.mcpServers // {}) | length)
+ (((.projects[$d].mcpServers) // {}) | length)
' "$cfg" 2>/dev/null)
[ -n "$mcpn" ] && [ "$mcpn" -gt 0 ] 2>/dev/null && bot+=" ${CYN}⇄ ${mcpn}${RST}"
fi
# ---- emit: environment line on top, session line below ----
# The note (📌) renders right-aligned at the END of the session line when width is known.
sline="${top# }"
if [ -n "$note" ]; then
{ [ -z "$cols" ] || [ "$cols" -lt 20 ] 2>/dev/null; } && cols="$COLUMNS" # Claude Code sets COLUMNS for the script
if [ -n "$cols" ] && [ "$cols" -gt 20 ] 2>/dev/null && command -v python3 >/dev/null 2>&1; then
pad=$(SLINE="$sline" NOTE="$note" COLS="$cols" python3 - <<'PY'
import os,re,unicodedata
def w(s):
s=re.sub(r'\x1b\[[0-9;]*m','',s) # strip SGR color
s=re.sub(r'\x1b\]8;;.*?\x1b\\','',s) # strip OSC8 hyperlink wrappers
n=0
for ch in s:
if unicodedata.combining(ch): continue
ea=unicodedata.east_asian_width(ch)
n += 2 if ea in ('W','F') or ord(ch) >= 0x1F000 else 1
return n
cols=int(os.environ['COLS']); sl=w(os.environ['SLINE']); nt=w(os.environ['NOTE'])
gap=cols - sl - nt
print(gap if gap > 1 else 1)
PY
)
printf '%s\n%s%*s%s' "${bot# }" "$sline" "$pad" "" "$note"
else
printf '%s\n%s %s' "${bot# }" "$sline" "$note"
fi
else
printf '%s\n%s' "${bot# }" "$sline"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment