Claude Code statusline: model name and context usage at a glance
Claude Code pipes a JSON payload into your statusline script. Read it, and the bar shows the project and branch you are on, the model, and color-coded context usage with token...

Claude Code lets you replace the status bar at the bottom of the terminal with the output of any command you like. The part most people miss is that it also pipes a JSON payload into that command every time it renders — the model in use, the context window size, how many tokens are in it, the working directory. So the status bar can answer the three questions I keep asking mid-session — where am I, which model is this, and how close am I to a /compact — without me typing anything.
Here’s where this ends up — a terminal status bar reading app-tools @ main | Fable 5.1 | ctx 9% (89k/1M), with the context part turning orange at 50% and red at 80%. The rest of this post is how to build it.
app-tools @ main | Fable 5.1 | ctx 9% (89k/1M)
Updated 2026-09-08. The first version of this post showed only the model name and a percentage badge next to a plugin badge. I have since dropped the plugin, added the current directory (clipped to the last three segments) and the token counts, and reduced the colors to orange and red — the two that mean something. The sample payload is a real capture from Claude Code 2.1.263.
Updated 2026-09-10. Clipping the path to three segments hid the one thing I actually needed. Every worktree of mine lives at <repo>/.claude/worktrees/<name>, so the last three segments are always .claude/worktrees/<name> — the project name is exactly what gets cut. Two projects with an android worktree produced the same status bar, and worktree names like docs or legal name the task, not the project. The script now asks git for the project instead of reading it off the path, and the version below is the one I run today.
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. Trimmed to the fields this post uses, the payload looks like this:
{
"model": {
"id": "claude-fable-5-1",
"display_name": "Fable 5.1"
},
"workspace": {
"current_dir": "/home/ivan/code/app-tools"
},
"context_window": {
"context_window_size": 1000000,
"current_usage": {
"input_tokens": 32,
"cache_creation_input_tokens": 806,
"cache_read_input_tokens": 88407
},
"used_percentage": 9,
"remaining_percentage": 91
}
}
model.display_nameis the model actually running the session — useful the moment you have more than one profile or switch models mid-project.workspace.current_diris the directory the session is working in. It follows you into a git worktree — though the raw path is the wrong thing to print there, for the reason in the 2026-09-10 note above.context_window.context_window_sizeis the size of the window in tokens. With a 1M window a percentage alone is easy to misread, so the script prints the raw numbers next to it.context_window.current_usageis what the last request actually sent: fresh input tokens, plus the tokens written to and read from the prompt cache. Their sum is the size of the conversation right now.context_window.used_percentageis the same thing as a percentage, precomputed. The script prefers it and falls back to the division when it is missing.
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 the script prints a bare Claude instead of erroring, so a missing dependency shows up as a boring status bar rather than a broken one. Two jq calls do the reading — one for the directory, one that pulls the model name and all three numbers at once — and the // 0 defaults keep the arithmetic safe when a field is absent. git is a soft dependency: if it is missing from the statusline’s PATH, or safe.directory rejects the directory, every git call fails quietly and the bar falls back to the clipped path — which is the ambiguous rendering this whole update exists to avoid, so it is worth knowing that it is the failure mode:
#!/usr/bin/env bash
# Claude Code status line: location + model name + context usage.
input=$(cat)
command -v jq >/dev/null || { echo "Claude"; exit 0; }
dir=$(printf '%s' "$input" | jq -r '.workspace.current_dir // .cwd // empty')
# Inside a repo, show "<project> @ <branch>" instead of the path. Worktrees live at
# <repo>/.claude/worktrees/<name>, so the last 3 path segments are always
# ".claude/worktrees/<name>" — the project name is exactly what gets truncated away,
# and worktree names alone ("android", "docs", "legal") are ambiguous across projects.
# The first entry of `git worktree list` is always the MAIN worktree, so this names the
# project from a linked worktree and from a submodule alike (deriving it from the git
# common dir instead reports "modules" inside a submodule).
loc=""
if [ -n "$dir" ]; then
# `env -u` matters: with GIT_DIR/GIT_WORK_TREE set (inside a git hook, say) `git -C`
# still reports THAT repo, so the bar would name a project the session is not in.
git_q() { env -u GIT_DIR -u GIT_WORK_TREE -u GIT_COMMON_DIR git -C "$dir" "$@" 2>/dev/null; }
# Match the "worktree " prefix rather than trusting line order. Strip a trailing .git
# so a bare repo does not read as "foo.git".
main=$(git_q worktree list --porcelain | sed -n 's/^worktree //p' | head -n 1)
if [ -n "$main" ]; then
loc=$(basename "$main"); loc=${loc%.git}
branch=$(git_q symbolic-ref --quiet --short HEAD) || branch=$(git_q rev-parse --short HEAD)
if [ -n "$branch" ]; then
[ ${#branch} -gt 24 ] && branch="${branch:0:23}…"
loc="$loc @ $branch"
fi
fi
fi
# Outside a repo: keep the previous behaviour, last 3 path segments.
if [ -z "$loc" ]; then
loc=${dir/#$HOME/\~}
IFS=/ read -ra parts <<< "${loc#/}"
if [ ${#parts[@]} -gt 3 ]; then loc="…/${parts[-3]}/${parts[-2]}/${parts[-1]}"; fi
fi
read -r model size used pct < <(printf '%s' "$input" | jq -r '
[ (.model.display_name // .model.id // "Claude" | gsub(" "; "_")),
(.context_window.context_window_size // 0),
(.context_window.current_usage | ((.input_tokens // 0) + (.cache_creation_input_tokens // 0) + (.cache_read_input_tokens // 0))),
(.context_window.used_percentage // "")
] | map(tostring) | join(" ")')
model=${model//_/ }
[ -n "$loc" ] && model="$loc | $model"
if [ "$size" = "0" ]; then printf '%s' "$model"; exit 0; fi
[ -z "$pct" ] && pct=$(( used * 100 / size ))
pct=${pct%.*}
k() { local n=$1; if [ "$n" -ge 1000000 ]; then printf '%dM' $(( n / 1000000 )); else printf '%dk' $(( (n + 500) / 1000 )); fi; }
color=""; reset=""
if [ "$pct" -ge 80 ]; then color=$'\e[31m'; reset=$'\e[0m' # red
elif [ "$pct" -ge 50 ]; then color=$'\e[38;5;208m'; reset=$'\e[0m' # orange
fi
printf '%s | %sctx %s%% (%s/%s)%s' "$model" "$color" "$pct" "$(k "$used")" "$(k "$size")" "$reset"
A few choices worth calling out:
- Project, not path. Inside a repository the bar shows
<project> @ <branch>, or the short commit instead of the branch on a detachedHEAD. The first entry ofgit worktree listis always the main worktree, so its directory name is the project, and the branch says which worktree you are in. Taking the parent ofgit rev-parse --git-common-dirinstead looks equivalent and is not: for a submodule atvendor/libthat common directory is<parent>/.git/modules/vendor/lib, whose parent isvendor— so the bar names the directory the submodule sits in rather than the submodule. Theenv -uwrapper matters for the same reason: withGIT_DIRorGIT_WORK_TREEset in the environment,git -Cstill reports that repository, and the bar would confidently name a project the session is not in. Outside a repository the old clipping still applies:$HOMEbecomes~, and anything deeper than three segments is cut to the last three. That costs twogitcalls, or three on a detached HEAD, and about 3 ms in total — invisible at statusline cadence. - Percentage first, tokens as a sanity check.
used_percentageis the number to watch.89k/1Mnext to it is what stops me from reading 9% as "almost empty" on a window that already holds almost ninety thousand tokens. - Color only where it matters. The directory and model name stay in the default color; only the
ctxpart changes — nothing under 50%, orange (256-color 208) from 50%, red from 80%. Two thresholds, two colors. A green badge that is always green is noise. - The percentage is truncated, not rounded.
${pct%.*}drops a fractional part so a42.7from the payload still survives the integer comparison instead of crashing the whole line.
The result
Terminal status bar now reads, left to right: where I am, which model is running, how full the context is.
~/Downloads | Fable 5.1 | ctx 9% (89k/1M)
two-notes @ main | Fable 5.1 | ctx 21% (210k/1M)
two-notes @ fix/legal-urls | Fable 5.1 | ctx 63% (630k/1M)
Default color under 50% context usage, orange 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 model, workspace and context_window 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.