Skip to content

Building a Claude Code Status Bar

A 40-line shell script on stdin: git branch, active model, and a color-coded context window bar that turns red before the session falls apart.

Matt Dennis

By the time you notice a Claude Code session is degrading, it’s usually too late to do anything about it. The model starts rewording instead of reasoning. Suggestions get shallower. You check the context window and it’s at 6%. The conversation you spent an hour building is gone.


Claude Code exposes a statusLine configuration key that runs an arbitrary shell command and displays the output in the terminal footer on every prompt. The command receives a JSON blob on stdin with the current directory, model name, and remaining context window percentage. You can render whatever you want.


I built a status bar. It looks like this:


➜  mattdennis.dev ⎇ main ✗  [sonnet ██████░░░░]

Left to right: working directory, git branch, dirty indicator, model name, context window bar.


The Config

One entry in ~/.claude/settings.json:


"statusLine": {
  "type": "command",
  "command": "bash ~/.claude/statusline-command.sh"
}

Claude Code pipes the session JSON into the script on every prompt and renders whatever the script prints to stdout. The input looks like:


{
  "workspace": { "current_dir": "/Users/matt/mattdennis.dev" },
  "model": { "display_name": "claude-sonnet-4-6" },
  "context_window": { "remaining_percentage": 61.4 }
}

The script pulls these with jq, builds the line, and prints it. No daemon, no polling, no persistent state.


Git State

The first thing I wanted was git context — which branch, and whether it’s dirty. The terminal prompt I use doesn’t show branch name inside the Claude Code UI, so I was constantly git status-ing to remember where I was.


cwd=$(echo "$input" | jq -r '.workspace.current_dir // ""')
branch=$(git -C "$cwd" branch --show-current 2>/dev/null)
dirty=$(git -C "$cwd" status --porcelain 2>/dev/null | head -1)

The git -C "$cwd" flag runs git against the session’s working directory, not wherever the script happens to be invoked from. Branch name gets a red glyph. If there’s anything in --porcelain output, a yellow appears next to it. Clean repos show nothing.


The Context Bar

The interesting part is the bar. The remaining percentage comes in as a float — 61.4, 23.0, 4.7. I wanted something visual rather than a number, so I render a 10-character block bar using and :


bar=$(python3 - "$pct" <<'PYEOF'
import sys
pct = int(sys.argv[1])
full_chars = round(pct * 10 / 100)
bar = '█' * full_chars + '░' * (10 - full_chars)
if pct < 25:
    color = '\033[31m'
elif pct < 50:
    color = '\033[33m'
else:
    color = '\033[32m'
print(f'{color}{bar}\033[0m', end='')
PYEOF
)

Shell arithmetic doesn’t handle floats cleanly, so the bar rendering drops into a Python heredoc. One invocation, no external dependencies. The bar is green above 50%, yellow from 25–49%, red below 25%. At 4%, you get ten blocks of red ░░░░░░░░░░ — visually impossible to ignore.


The thresholds map to real urgency. Above 50% the session has plenty of room to maneuver. Below 25% is when I start thinking about whether to wrap up or start a new session. Below 10% is when the model’s behavior starts to change regardless of what you want.


Model Name

The model display name comes directly from the session JSON. I extract it and render it next to the bar as a label:


model=$(echo "$input" | jq -r '.model.display_name // ""')

The script hardcodes sonnet as the label text rather than the full model ID — claude-sonnet-4-6 is more noise than signal when it’s on every prompt. When model or percentage are missing (the first prompt of a session, before context has accumulated), the bar is omitted entirely.


The Full Line

The bar assembles left to right, each segment conditional on the data being present:


➜  mattdennis.dev ⎇ main  [sonnet ██████████]   # clean, full context
➜  mattdennis.dev ⎇ main ✗  [sonnet ████░░░░░░]  # dirty, 40% remaining
➜  mattdennis.dev ⎇ main  [sonnet ██░░░░░░░░]   # clean, 20% — yellow
➜  mattdennis.dev ⎇ main  [sonnet ░░░░░░░░░░]   # below 10% — all red

The color rendering depends on the terminal supporting ANSI escape codes. Claude Code’s built-in terminal does. If the script is ever invoked somewhere that doesn’t, the colors are noise — but that hasn’t come up.


What It Changed

Mostly: I’m no longer surprised by context exhaustion. The bar is in peripheral vision every time I type something. The yellow threshold gives me enough time to decide whether to start a new session or push through to a natural stopping point. The red threshold means I’ve already made that choice and I’m in cleanup mode.


The git state matters more than I expected. When jumping between projects mid-day, I’d occasionally find myself three commands into a session before realizing I was in the wrong repo. The branch name in the footer is enough to catch that before it causes damage.


The whole script is about 40 lines of shell with one embedded Python fragment. It runs fast enough that I’ve never noticed latency — the bar updates are imperceptible compared to the model’s response time.


The statusLine key accepts any shell command. The JSON schema it sends is documented but shallow — there’s more useful data in there than I’m currently using. The remaining percentage is the one that changed my workflow.