Skip to content

Instantly share code, notes, and snippets.

@ohadperry
Last active July 13, 2026 14:43
Show Gist options
  • Select an option

  • Save ohadperry/984aa915c3e3b6695b2c14f0cc1c1f4d to your computer and use it in GitHub Desktop.

Select an option

Save ohadperry/984aa915c3e3b6695b2c14f0cc1c1f4d to your computer and use it in GitHub Desktop.
Claude Code status line
#!/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