- Book: LLM Observability Pocket Guide
- The series: Pocket Guides for Developers — 8 short books on agents, RAG, prompts, observability, events, databases and system design — the whole series
- My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools
- Me: xgabriel.com | GitHub
You have three Claude Code sessions open, and you want to know which one is on the worktree you're about to delete. So you type git branch in each terminal. Claude Code already knows the answer, and it will hand it to any script you name.
Every time a new assistant message arrives, /compact finishes, the permission mode changes, and on a few other events, Claude Code runs your statusLine command and pipes it a JSON object on stdin. Whatever the script prints shows up as a row at the bottom of the session. Updates are debounced at 300ms, the script runs locally, and per the docs it "does not consume API tokens".
Below are six small scripts that read real fields from that payload. Every field name comes from the status line docs and the changelog as of 2.1.302, released October 16, 2026. I haven't run these scripts myself, so the first section gives you a mock payload to test them against before you trust them.
Wire it up, and test without a session
Add this to ~/.claude/settings.json:
{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh",
"refreshInterval": 5
}
}
refreshInterval is optional. It re-runs the command every N seconds on top of the event triggers, with a minimum of 1. The docs suggest it for when background subagents change git state while the main session sits idle. Leave it out and the script runs on events only.
Scripts 1 to 5 are alternatives: put whichever one you want in ~/.claude/statusline.sh, and script 6 gets its own file. To combine two, read input=$(cat) once and print two lines, which the docs say become two rows. Make the file executable with chmod +x ~/.claude/statusline.sh and install jq if you don't have it. Then test it with a mock payload, built from the full JSON schema in the docs with the fields trimmed down:
cat > mock.json <<'EOF'
{
"model": {"id": "claude-opus-5-5", "display_name": "Opus"},
"workspace": {"current_dir": "/tmp",
"git_worktree": "fix-login"},
"cost": {"total_cost_usd": 0.01234,
"total_duration_ms": 45000,
"total_lines_added": 156,
"total_lines_removed": 23},
"context_window": {"context_window_size": 200000,
"used_percentage": 8},
"effort": {"level": "high"},
"fast_mode": false
}
EOF
~/.claude/statusline.sh < mock.json
The script has to exit with code 0 and print to stdout, or the row goes blank. If it stays empty inside a session, claude --debug logs the script's stderr on every run. A folder you haven't accepted the workspace trust dialog for also keeps the row blank, because statusLine runs a shell command under the same trust rule as hooks.
Scripts 1 and 2: branch and worktree
The payload doesn't carry the branch of an ordinary checkout, so script 1 asks git, in the directory the payload names:
#!/bin/bash
# script 1: folder and git branch
input=$(cat)
DIR=$(jq -r '.workspace.current_dir' <<<"$input")
BRANCH=$(git -C "$DIR" branch --show-current 2>/dev/null)
echo "${DIR##*/}${BRANCH:+ on $BRANCH}"
The row reads <folder> on <branch>. In detached HEAD state git branch --show-current prints nothing, so ${BRANCH:+...} drops the "on" part instead of leaving it dangling. This calls git on every update. In a large repository, the docs' caching example writes the result to a temp file keyed by session_id and refreshes it every 5 seconds. Copy that if the row starts to lag.
Script 2 answers the question from the opening. There are two worktree fields, and they mean different things:
-
workspace.git_worktreeis the worktree name whenever the current directory is inside a linked worktree made withgit worktree add. It is absent in the main working tree. -
worktree.name,worktree.branchand friends are present only while the session is in one of Claude Code's own worktree sessions.branchcan be absent for hook-based worktrees.
#!/bin/bash
# script 2: which worktree is this session in?
input=$(cat)
WT=$(jq -r '.worktree.name // .workspace.git_worktree // empty' \
<<<"$input")
BR=$(jq -r '.worktree.branch // empty' <<<"$input")
if [ -n "$WT" ]; then
echo "wt:$WT${BR:+ ($BR)}"
else
echo "main checkout"
fi
The // operator falls through to the next field when the first is missing, and empty prints nothing at all, so the main checkout gets its own label. Because git_worktree covers any linked worktree, the label also works when another tool made the worktree, Hermes included.
Scripts 3 to 5: model, cost and context
Script 3 is pure jq. effort.level is absent when the model doesn't support the effort parameter, and empty drops it from the array instead of printing null:
#!/bin/bash
# script 3: model, effort, fast mode
jq -r '[ (.model.display_name // "?"),
(.effort.level // empty),
(if .fast_mode then "fast" else empty end)
] | join(" · ")'
The row reads <model> · <effort>, plus · fast when fast mode is on. Effort reflects the live session value, so a mid-session /effort change shows up on the next update.
Script 4 reads the cost object:
#!/bin/bash
# script 4: session cost, minutes, lines changed
export LC_NUMERIC=C # printf %.2f wants a dot, not a comma
input=$(cat)
read -r COST MS ADD DEL <<<"$(jq -r '[
.cost.total_cost_usd // 0, .cost.total_duration_ms // 0,
.cost.total_lines_added // 0, .cost.total_lines_removed // 0
] | @tsv' <<<"$input")"
printf '$%.2f · %dm · +%d -%d\n' \
"$COST" $((MS / 60000)) "$ADD" "$DEL"
Read that dollar figure with care. cost.total_cost_usd is an estimate computed client-side at list price, unless a modelPricing table is in effect, and the docs say it may differ from your actual bill. It resets to $0 when /clear starts a new session. On a Pro or Max plan, the rate_limits.five_hour and rate_limits.seven_day percentages show how much of your 5-hour and 7-day limits you've used, and the docs have a ready-made script for them.
Script 5 turns context_window.used_percentage into a ten-cell bar that goes green, yellow, then red, the same thresholds as the docs' multi-line example:
#!/bin/bash
# script 5: context bar, green / yellow / red
input=$(cat)
read -r PCT SIZE <<<"$(jq -r '[
(.context_window.used_percentage // 0 | floor),
(.context_window.context_window_size // 0)
] | @tsv' <<<"$input")"
[ "$PCT" -gt 100 ] && PCT=100
if [ "$PCT" -ge 90 ]; then C='\033[31m'
elif [ "$PCT" -ge 70 ]; then C='\033[33m'
else C='\033[32m'; fi
N=$((PCT / 10))
BAR=$(printf '%*s' "$N" '' | tr ' ' '#')
BAR+=$(printf '%*s' $((10 - N)) '' | tr ' ' '-')
S="$((SIZE / 1000))k"
[ "$SIZE" -ge 1000000 ] && S="$((SIZE / 1000000))M"
printf '%b%s%b %s%% of %s\n' "$C" "$BAR" '\033[0m' "$PCT" "$S"
Three details from the docs matter here. used_percentage can be null early in a session, hence the // 0. It counts input tokens only (input, cache writes and cache reads), not output. And it comes from the last API response, while /context adds an estimate for messages since then, so /context can read higher until the next response. context_window_size is 200000 by default and 1000000 on models with extended context, which is why the script prints 200k or 1M.
Script 6: subagent rows by agentType
The main status line describes the session. Subagents get their own setting, subagentStatusLine, which replaces the default name · description · token count row in the agent panel:
{
"subagentStatusLine": {
"type": "command",
"command": "~/.claude/subagent-statusline.sh"
}
}
This one has a different contract. It runs once per refresh tick and receives every visible row in one JSON object: a columns width and a tasks array. You write back one JSON line per row you want to change, in the form {"id": "...", "content": "..."}. Leave a task's id out and it keeps the default row. Send an empty content and the row is hidden.
Version 2.1.293 added agentType to each task, "so scripts can tell custom subagent types apart". It holds the built-in type, such as Explore, or your own, such as code-reviewer:
#!/bin/bash
# subagent-statusline.sh: one row per subagent
jq -c '.tasks[]? | {
id,
content: ([
(.agentType // "agent"),
(.status // "?"),
"\((.tokenCount // 0) / 1000 | floor)k tok",
(.model // empty)
] | join(" · "))
}'
Each row reads <agentType> · <status> · <N>k tok · <model id>. model is left out until Claude Code resolves it, and on versions before 2.1.293 there is no agentType, so the row falls back to agent. Tasks also carry contextWindowSize once the model is known, and tokenSamples, up to 16 recent readings, if you want a per-row percentage or a sparkline.
What one status line can't show you
These six scripts describe one Claude Code session from inside it. Open five agents and the gaps show:
- Five rows, five places to look. Each status line lives in its own terminal. No status line tells you which of the five is waiting on you.
- It hides when the agent needs you. The docs say the row "temporarily hides during certain UI interactions, including the help menu and permission prompts".
-
It goes quiet when idle. Updates are event-driven, and the docs say they can go quiet while the session is idle unless you set
refreshInterval. -
It is Claude Code only. Codex CLI has its own
tui.status_line, an ordered list of footer item identifiers. Other agents have their own setups or none. Run several vendors and you get several formats.
If you only run Claude Code, the scripts above, or community setups like ccstatusline and starship-claude, may be all you need.
The multi-agent case is the one I build Hermes IDE for, so weigh this with that in mind. Hermes runs each agent's own interface in a real terminal, so your Claude Code status line still renders there. Around those terminals it gives every session, from any vendor, one status: working, needs approval, asked you, done, error or idle, and it says when a status is a guess. The attention inbox counts the agents blocked on you, and ⌘I (Ctrl+Shift+I on Windows and Linux) jumps to the one that has waited longest.

Codex and Claude Code side by side, each in its own hermes/ worktree, with status, context use, cost and branch per session in the sidebar.
Each session also shows its spend: the cost the agent reports, or an estimate from its transcript, marked as estimated, with token totals. Since 2.3.4, the bar above each session works the same way for every agent, and a context meter shows how full the conversation is, offering Compact when it gets close to full.

A Claude Code session after pressing the Context chip: Claude Code's own /context breakdown, the context gauges per session in the sidebar, and the token total and estimated cost in the status strip.
Hermes is free, source-available under BSL 1.1, and runs on macOS, Windows and Linux. It isn't affiliated with Nous Research, Anthropic, OpenAI or any agent vendor.
If this was useful
A status line is the smallest piece of observability you can give an agent: what it costs, how full its context is, what it's working on. The LLM Observability Pocket Guide covers what comes after the status bar, from tracing and evals to token cost, and how to pick tools for your team. It's one of 8 short books in the Pocket Guides for Developers series, covering agents, RAG, prompts, observability, events, databases and system design. If you run more than one agent a day, download Hermes IDE or read the source on GitHub.

Top comments (0)