Last active
July 13, 2026 14:43
-
-
Save ohadperry/984aa915c3e3b6695b2c14f0cc1c1f4d to your computer and use it in GitHub Desktop.
Claude Code status line
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 | |
| # Claude Code status line — dir · git · model · context bar, plus a live, | |
| # per-session "mission control" block: Goal / Done / Now / Next. | |
| # | |
| # The agent rewrites the block every turn, so you always know what it's doing. | |
| # | |
| # storefront git:(feature/checkout) · Opus 4.8 · ctx: [████████░░░░░░░░░░░░] 42% | |
| # ✳ Goal: add checkout to the cart page | |
| # Done: built /api/checkout endpoint + tests | |
| # Now: wiring the frontend "Pay" button | |
| # Next: handle the payment-failed state | |
| # | |
| # ── SETUP ───────────────────────────────────────────────────────────────────── | |
| # 1. Save this file to ~/.claude/statusline-command.sh and make it executable: | |
| # chmod +x ~/.claude/statusline-command.sh | |
| # | |
| # 2. Point Claude Code at it — in ~/.claude/settings.json: | |
| # "statusLine": { | |
| # "type": "command", | |
| # "command": "bash /Users/YOU/.claude/statusline-command.sh" | |
| # } | |
| # | |
| # 3. (The magic) Auto-create the block for EVERY new session — add this hook to | |
| # ~/.claude/settings.json so you never have to set it up by hand: | |
| # "hooks": { | |
| # "SessionStart": [ | |
| # { "hooks": [ { | |
| # "type": "command", | |
| # "command": "jq -r '.session_id // empty' | { read -r sid; [ -n \"$sid\" ] || exit 0; d=\"$HOME/.claude/session-status\"; f=\"$d/${sid}.txt\"; mkdir -p \"$d\"; [ -f \"$f\" ] || printf 'Goal: (describe this session)\\nDone: -\\nNow: session started\\nNext: -\\n' > \"$f\"; }" | |
| # } ] } | |
| # ] | |
| # } | |
| # It seeds ~/.claude/session-status/<session_id>.txt only if it doesn't exist, | |
| # keyed on session_id so parallel sessions never clash. Restart Claude Code. | |
| # | |
| # GOTCHA: the seed above uses a plain "-" on purpose. Hooks run under POSIX | |
| # printf, which does NOT interpret backslash-u escapes. Putting the six | |
| # characters \u2014 in the format string emits them verbatim, not an | |
| # em-dash. If you want a real "—" in the seed, paste the literal — character. | |
| # | |
| # 4. Keep it live. The seed is just a placeholder — the agent must rewrite the | |
| # file each turn for Goal/Now/Next to mean anything. Paste this into | |
| # ~/.claude/CLAUDE.md so EVERY session maintains it automatically: | |
| # | |
| # ## Live Session Status Line | |
| # - Find this session's session_id (the folder name in the scratchpad path | |
| # Claude Code gives you, e.g. .../<session_id>/scratchpad) and maintain | |
| # ~/.claude/session-status/<session_id>.txt. | |
| # - Write exactly four lines, one item each, no blank lines: | |
| # Goal: <one-line objective for this session> | |
| # Done: <most recent completed step, or - if none> | |
| # Now: <what you are doing this turn> | |
| # Next: <the immediate next step, or - if unknown> | |
| # - Overwrite (never append) at the start of each turn and whenever the | |
| # plan changes. Do it silently — do not narrate it or ask permission. | |
| # | |
| # (Or, for one session only, just tell the agent: "keep my status line updated | |
| # — rewrite ~/.claude/session-status/<my session_id>.txt as the plan changes.") | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| input=$(cat) | |
| cwd=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // empty') | |
| [ -z "$cwd" ] && cwd=$(pwd) | |
| dir=$(basename "$cwd") | |
| # Per-session status block (maintained by the assistant; seeded by the hook above). | |
| session_id=$(echo "$input" | jq -r '.session_id // empty') | |
| session_status="" | |
| status_file="$HOME/.claude/session-status/${session_id}.txt" | |
| if [ -n "$session_id" ] && [ -f "$status_file" ]; then | |
| session_status=$(head -c 1200 "$status_file") # keep newlines: one line = one rendered line | |
| fi | |
| model=$(echo "$input" | jq -r '.model.display_name // empty') | |
| used_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty') | |
| used_pct=${used_pct%%.*} # floor to integer, in case a fractional % is ever reported | |
| build_bar() { | |
| local pct="$1" | |
| local bar_width=20 | |
| local filled=$(( pct * bar_width / 100 )) | |
| # Color: green <= 60%, yellow <= 85%, red > 85% | |
| if [ "$pct" -le 60 ]; then bar_color="\033[0;32m" | |
| elif [ "$pct" -le 85 ]; then bar_color="\033[0;33m" | |
| else bar_color="\033[0;31m" | |
| fi | |
| local bar="" i=0 | |
| while [ $i -lt $filled ]; do bar="${bar}█"; i=$(( i + 1 )); done | |
| while [ $i -lt $bar_width ]; do bar="${bar}░"; i=$(( i + 1 )); done | |
| printf "${bar_color}[%s]\033[0m %d%%" "$bar" "$pct" | |
| } | |
| # Git branch (skip optional locks), with a dirty marker. | |
| git_branch="" | |
| if git -C "$cwd" rev-parse --git-dir > /dev/null 2>&1; then | |
| branch=$(git -C "$cwd" symbolic-ref --short HEAD 2>/dev/null \ | |
| # plan changes. Do it silently — do not narrate it or ask permission. | |
| # | |
| # (Or, for one session only, just tell the agent: "keep my status line updated | |
| # — rewrite ~/.claude/session-status/<my session_id>.txt as the plan changes.") | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| input=$(cat) | |
| cwd=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // empty') | |
| [ -z "$cwd" ] && cwd=$(pwd) | |
| dir=$(basename "$cwd") | |
| # Per-session status block (maintained by the assistant; seeded by the hook above). | |
| session_id=$(echo "$input" | jq -r '.session_id // empty') | |
| session_status="" | |
| status_file="$HOME/.claude/session-status/${session_id}.txt" | |
| if [ -n "$session_id" ] && [ -f "$status_file" ]; then | |
| session_status=$(head -c 1200 "$status_file") # keep newlines: one line = one rendered line | |
| fi | |
| model=$(echo "$input" | jq -r '.model.display_name // empty') | |
| used_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty') | |
| used_pct=${used_pct%%.*} # floor to integer, in case a fractional % is ever reported | |
| build_bar() { | |
| local pct="$1" | |
| local bar_width=20 | |
| local filled=$(( pct * bar_width / 100 )) | |
| # Color: green <= 60%, yellow <= 85%, red > 85% | |
| if [ "$pct" -le 60 ]; then bar_color="\033[0;32m" | |
| What changed vs. the current gist (3 edits, all in the comment header + one dead line): | |
| 1. Added the GOTCHA note (lines ~37–40) about \u2014 not being interpreted by POSIX printf. | |
| 2. Rewrote Step 4 into a concrete, copy-paste ## Live Session Status Line CLAUDE.md directive. | |
| left="\033[0;36m${dir}\033[0m${git_branch}" | |
| # Top block: session status (dim). First line gets ✳; the rest are indented. | |
| if [ -n "$session_status" ]; then | |
| first=1 | |
| while IFS= read -r sline || [ -n "$sline" ]; do | |
| if [ "$first" -eq 1 ]; then | |
| printf "\033[2m✳ %s\033[0m\n" "$sline"; first=0 | |
| else | |
| printf "\033[2m %s\033[0m\n" "$sline" | |
| fi | |
| done <<< "$session_status" | |
| fi | |
| # Bottom line: dir · git · model · context bar. | |
| if [ -n "$model" ] && [ -n "$used_pct" ]; then | |
| bar=$(build_bar "$used_pct") | |
| printf "%b \033[0;35m%s\033[0m ctx: %b\n" "$left" "$model" "$bar" | |
| elif [ -n "$model" ]; then | |
| printf "%b \033[0;35m%s\033[0m\n" "$left" "$model" | |
| else | |
| printf "%b\n" "$left" | |
| fi | |
| || git -C "$cwd" describe --tags --exact-match 2>/dev/null \ | |
| || git -C "$cwd" rev-parse --short HEAD 2>/dev/null) | |
| if [ -n "$branch" ]; then | |
| if git -C "$cwd" --no-optional-locks status --porcelain 2>/dev/null | grep -q .; then | |
| dirty=" \033[33m✗\033[0m" | |
| else | |
| dirty="" | |
| fi | |
| git_branch=" \033[1;34mgit:(\033[0;31m${branch}\033[1;34m)${dirty}\033[0m" | |
| fi | |
| fi | |
| left="\033[0;36m${dir}\033[0m${git_branch}" | |
| # Top block: session status (dim). First line gets ✳; the rest are indented. | |
| if [ -n "$session_status" ]; then | |
| first=1 | |
| while IFS= read -r sline || [ -n "$sline" ]; do | |
| if [ "$first" -eq 1 ]; then | |
| printf "\033[2m✳ %s\033[0m\n" "$sline"; first=0 | |
| else | |
| printf "\033[2m %s\033[0m\n" "$sline" | |
| fi | |
| done <<< "$session_status" | |
| fi | |
| # Bottom line: dir · git · model · context bar. | |
| if [ -n "$model" ] && [ -n "$used_pct" ]; then | |
| bar=$(build_bar "$used_pct") | |
| printf "%b \033[0;35m%s\033[0m ctx: %b\n" "$left" "$model" "$bar" | |
| elif [ -n "$model" ]; then | |
| printf "%b \033[0;35m%s\033[0m\n" "$left" "$model" | |
| else |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment