Claude Code statusline: model name and context usage at a glance
Claude Code pipes a JSON blob into your statusline script after certain events (debounced, not on every keystroke). Two fields — model.display_name and...
I run Claude Code with a colored badge in the terminal status bar that shows when a custom mode ("caveman", a compression persona from a community plugin) is active. It is a small thing, but it works because the status bar always shows the right state without me asking. So the obvious next question was: what else can I put there? Turns out Claude Code already hands your statusline script everything it needs — you just have to read it.
Claude Code feeds your statusline script a JSON blob on stdin
If you configure a custom statusline in settings.json:
{
"statusLine": {
"type": "command",
"command": "bash /path/to/your-statusline.sh"
}
}
Claude Code runs that command after certain events — an assistant response, a /compact, a few others — and pipes a JSON object into its stdin each time. Calls are debounced, not fired on every single keystroke. To see the payload for yourself, temporarily point statusLine.command at a capture command instead of your script:
"command": "tee /tmp/statusline-debug.json >/dev/null"
Trigger a render (send a message, or run /compact), then open /tmp/statusline-debug.json and swap the real script back in. Among other fields, you get exactly what you want for an at-a-glance status bar:
{
"model": {
"id": "claude-sonnet-5",
"display_name": "Sonnet 5"
},
"context_window": {
"used_percentage": 10,
"remaining_percentage": 90
}
}
model.display_name is the model actually running the session — useful the moment you have more than one profile or switch models mid-project. context_window.used_percentage is how full the context window is right now, which is the number that tells you when to run /compact instead of guessing.
The full script
Read the complete stdin with cat — not a truncated head, some fields (like workspace.added_dirs) are variable-length and a hard byte cap can cut a valid payload before jq gets to it. This needs jq on your PATH — without it, both badges silently stay blank instead of erroring, since stderr goes to /dev/null on purpose. Pull both fields, print a badge for each if present. The context badge gets a color ramp so red means "stop and compact" without reading the number, and a cut after jq so a fractional percentage (42.7) still renders instead of silently vanishing:
#!/bin/bash
# statusline.sh — model name + color-coded context usage
STDIN_JSON=$(cat 2>/dev/null)
# Model name badge.
MODEL_NAME=$(printf '%s' "$STDIN_JSON" | jq -r '.model.display_name // empty' 2>/dev/null | tr -d '[:cntrl:]')
if [ -n "$MODEL_NAME" ]; then
printf '\033[38;5;38m[%s]\033[0m ' "$MODEL_NAME"
fi
# Context usage badge: color ramps green→yellow→red as the window fills up.
# The `cut` truncates a fractional percentage (e.g. 42.7) to an integer so the
# numeric guard below still accepts it instead of dropping the badge.
CTX_PCT=$(printf '%s' "$STDIN_JSON" | jq -r '.context_window.used_percentage // empty' 2>/dev/null | cut -d. -f1 | tr -d '[:cntrl:]')
case "$CTX_PCT" in
''|*[!0-9]*) ;;
*)
if [ "$CTX_PCT" -ge 80 ]; then CTX_COLOR=196 # red
elif [ "$CTX_PCT" -ge 50 ]; then CTX_COLOR=214 # yellow
else CTX_COLOR=71 # green
fi
printf '\033[38;5;%sm[ctx %s%%]\033[0m ' "$CTX_COLOR" "$CTX_PCT"
;;
esac
exit 0
Save it, point statusLine.command at it, and both badges show up on the next render. tr -d '[:cntrl:]' strips control bytes before anything reaches the terminal, and the case guard (''|*[!0-9]*) skips the block instead of feeding a bad value into a numeric comparison and crashing the whole statusline — treat the JSON as untrusted input, not just convenient data, since this script runs unattended after every statusline update.
The result
Terminal status bar now reads, left to right:
[Sonnet 5] [ctx 42%] [CAVEMAN]
Green under 50% context usage, yellow from 50–79%, red from 80% up. When it turns red, I run /compact instead of finding out the hard way that the conversation is about to get summarized mid-task.
None of this needed a plugin. The context_window and model fields are part of the stdin payload Claude Code already sends to any statusline command — the only step most people skip is capturing stdin once and reading what is actually in there.