cheat sheet
Claude Code Statusline
Customise the Claude Code statusline — settings.json keys, the JSON context passed to scripts, available variables (model, session id, cwd, branch, cost), and example scripts in bash, python, and Node.
Claude Code Statusline
What it is
The Claude Code statusline is the single line of text rendered at the bottom of the interactive REPL. By default it shows the active model and cwd; with a custom statusLine configured in settings.json, the harness runs your script every few seconds, feeds it a JSON context blob (model, session id, cwd, transcript path, token usage, cost), and renders the script's stdout verbatim. It's the ambient HUD for a Claude session — most useful for showing git branch, session cost, last-tool indicator, or any project-specific signal you'd otherwise have to ask /status for. The closest cousin in other tooling is the shell prompt (PS1 / Starship); the statusline is to Claude what Starship is to your shell.
Enabling a custom statusline
Add a statusLine block to settings.json. The type is always command today; the command is a shell command-line that prints one line of output to stdout.
{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh"
}
}
Restart claude (or run /config then save) and the new statusline takes over.
chmod +x ~/.claude/statusline.sh
Output: (none — exits 0 on success)
JSON context on stdin
Each time the statusline refreshes, the harness writes a JSON blob to the script's stdin. Read it and emit one line of text.
{
"hook_event_name": "Status",
"session_id": "sess_01abc...",
"transcript_path": "/home/alice/.claude/projects/<hash>/sess_01abc.jsonl",
"cwd": "/home/alice/Code/myproject",
"model": {
"id": "claude-sonnet-4-6",
"display_name": "Sonnet 4.6"
},
"workspace": {
"current_dir": "/home/alice/Code/myproject",
"project_dir": "/home/alice/Code/myproject"
},
"version": "1.x.x",
"output_style": {
"name": "default"
},
"cost": {
"total_cost_usd": 0.1234,
"total_lines_added": 42,
"total_lines_removed": 17,
"total_api_duration_ms": 8420,
"total_duration_ms": 9210
}
}
The exact fields available are documented in the table below.
Available context fields
| Field | Type | Description |
|---|---|---|
session_id | string | Opaque session identifier (sess_01...) |
transcript_path | string | Absolute path to the session JSONL transcript |
cwd | string | Current working directory |
model.id | string | Active model ID (e.g. claude-sonnet-4-6) |
model.display_name | string | Human-readable model name |
workspace.current_dir | string | Same as cwd (kept for plugin compatibility) |
workspace.project_dir | string | Project root if Claude detected one |
version | string | claude-code binary version |
output_style.name | string | Current output style (default, concise, ...) |
cost.total_cost_usd | number | Cumulative session cost in USD |
cost.total_lines_added | number | Lines added across the session |
cost.total_lines_removed | number | Lines removed across the session |
cost.total_api_duration_ms | number | Time spent waiting on the API |
cost.total_duration_ms | number | Wall-clock time since session start |
Additional fields may appear in future versions; treat unknown keys as informational.
Minimal bash example
A one-line bash statusline that shows model and cwd. Drop it at ~/.claude/statusline.sh.
#!/usr/bin/env bash
# ~/.claude/statusline.sh
INPUT=$(cat)
MODEL=$(echo "$INPUT" | jq -r '.model.display_name // "?"')
CWD=$(echo "$INPUT" | jq -r '.cwd // ""')
printf " %s %s" "$MODEL" "$(basename "$CWD")"
Output:
Sonnet 4.6 myproject
Richer bash example
Adds git branch, last commit SHA, and session cost.
#!/usr/bin/env bash
# ~/.claude/statusline.sh
set -euo pipefail
INPUT=$(cat)
MODEL=$(echo "$INPUT" | jq -r '.model.display_name // "?"')
CWD=$(echo "$INPUT" | jq -r '.cwd // ""')
COST=$(echo "$INPUT" | jq -r '.cost.total_cost_usd // 0')
SID=$(echo "$INPUT" | jq -r '.session_id // ""' | cut -c1-12)
BRANCH=$(git -C "$CWD" branch --show-current 2>/dev/null || echo "-")
SHA=$(git -C "$CWD" rev-parse --short HEAD 2>/dev/null || echo "-")
# Format cost as "$0.12"
COST_FMT=$(printf '$%.2f' "$COST")
printf " %s %s %s@%s %s %s" \
"$MODEL" "$(basename "$CWD")" "$BRANCH" "$SHA" "$COST_FMT" "$SID"
Output:
Sonnet 4.6 myproject main@a3f12c $0.12 sess_01abcdef
Python example
Python is convenient when you need richer formatting or want to consult a remote service. The harness imposes a soft 1-second deadline per refresh — keep the script fast.
#!/usr/bin/env python3
# ~/.claude/statusline.py
import json, sys, subprocess, os
ctx = json.load(sys.stdin)
model = ctx.get("model", {}).get("display_name", "?")
cwd = ctx.get("cwd", "")
cost = ctx.get("cost", {}).get("total_cost_usd", 0.0)
def git(*args):
try:
return subprocess.check_output(["git", "-C", cwd, *args],
stderr=subprocess.DEVNULL).decode().strip()
except Exception:
return ""
branch = git("branch", "--show-current") or "-"
sha = git("rev-parse", "--short", "HEAD") or "-"
dirty = "*" if git("status", "--porcelain") else ""
print(f" {model} {os.path.basename(cwd)} {branch}{dirty}@{sha} ${cost:.2f}")
Output:
Sonnet 4.6 myproject main*@a3f12c $0.12
Then wire it up:
{
"statusLine": {
"type": "command",
"command": "python3 ~/.claude/statusline.py"
}
}
Node example
For projects already invested in Node, a JS statusline is one require away.
#!/usr/bin/env node
// ~/.claude/statusline.mjs
import { execSync } from "node:child_process";
import { basename } from "node:path";
const ctx = JSON.parse(await new Response(process.stdin).text());
const model = ctx.model?.display_name ?? "?";
const cwd = ctx.cwd ?? "";
const cost = ctx.cost?.total_cost_usd ?? 0;
const git = (args) => {
try { return execSync(`git -C "${cwd}" ${args}`, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim(); }
catch { return ""; }
};
const branch = git("branch --show-current") || "-";
const sha = git("rev-parse --short HEAD") || "-";
const dirty = git("status --porcelain") ? "*" : "";
process.stdout.write(` ${model} ${basename(cwd)} ${branch}${dirty}@${sha} $${cost.toFixed(2)}`);
Output:
Sonnet 4.6 myproject main*@a3f12c $0.12
{
"statusLine": {
"type": "command",
"command": "node ~/.claude/statusline.mjs"
}
}
Color and styling
The harness renders the script's stdout verbatim, so ANSI color codes pass through. Wrap segments in escape sequences for color.
#!/usr/bin/env bash
INPUT=$(cat)
MODEL=$(echo "$INPUT" | jq -r '.model.display_name')
PURPLE=$'\033[35m'
GRAY=$'\033[90m'
RESET=$'\033[0m'
printf "${PURPLE}%s${RESET} ${GRAY}%s${RESET}" "$MODEL" "in $(pwd)"
Output:
Sonnet 4.6 in /home/alice/Code/myproject
Respect NO_COLOR=1 for users who disable color:
if [ -n "${NO_COLOR:-}" ]; then
PURPLE=""; GRAY=""; RESET=""
fi
Output: (none — exits 0 on success)
Multi-line statuslines
The terminal only shows the first newline-terminated line. If your script prints multiple lines, only the first is rendered. Concatenate segments with separators rather than newlines.
# WRONG — only the first line shows
printf "model: %s\nbranch: %s\n" "$MODEL" "$BRANCH"
# RIGHT — single line with separators
printf "model: %s branch: %s" "$MODEL" "$BRANCH"
Output:
model: Sonnet 4.6 branch: main
Refresh behavior
The harness refreshes the statusline on every conversation turn, on certain hook events, and on a slow background timer (every ~5 seconds when the session is idle). The script is short-lived: it runs to completion each refresh, then the harness caches the output until the next refresh.
| Trigger | Frequency |
|---|---|
| Conversation turn ends | Always |
| Tool call completes | Always |
/compact, /clear, /model | Always |
| Background tick | ~5s when idle |
| User keystroke | Never (avoids flicker) |
Keep the script under 100ms. Slow scripts make the REPL feel laggy because the statusline blocks the next prompt render.
Project-scoped statusline
Statusline is configurable in .claude/settings.json too, so a team can ship a project-specific HUD that includes, say, the active feature branch and a CI status badge.
{
"statusLine": {
"type": "command",
"command": ".claude/statusline.sh"
}
}
#!/usr/bin/env bash
# .claude/statusline.sh — committed to the repo
INPUT=$(cat)
BRANCH=$(git branch --show-current 2>/dev/null || echo "-")
CI_STATUS=$(gh run list --branch "$BRANCH" --limit 1 --json status -q '.[0].status' 2>/dev/null || echo "?")
printf " %s CI:%s" "$BRANCH" "$CI_STATUS"
Output:
feature/jwt-auth CI:completed
Performance patterns
Cache expensive lookups
If you call gh run list or hit a remote API, cache the result in a temp file with a 30-second TTL.
CACHE=/tmp/claude-statusline-cache
if [ ! -f "$CACHE" ] || [ "$(find "$CACHE" -mmin +0.5 2>/dev/null)" ]; then
gh run list --limit 1 --json status -q '.[0].status' > "$CACHE" 2>/dev/null
fi
STATUS=$(cat "$CACHE" 2>/dev/null || echo "?")
printf "CI:%s" "$STATUS"
Output:
CI:completed
Skip work when idle
The harness re-runs the statusline frequently. If your script does costly work, gate it on whether any context has changed.
import json, sys, os, hashlib, pathlib
ctx = json.load(sys.stdin)
key = hashlib.md5(json.dumps({k: ctx.get(k) for k in ("session_id","cwd","model")}).encode()).hexdigest()
cache = pathlib.Path(f"/tmp/cc-status-{key}")
if cache.exists() and cache.stat().st_mtime > __import__("time").time() - 5:
print(cache.read_text())
sys.exit(0)
# ... do the slow lookup ...
Output: (none — exits 0 on success)
Useful indicators
A non-exhaustive list of indicators worth surfacing in the statusline. Mix and match to taste.
| Indicator | Source |
|---|---|
| Model | ctx.model.display_name |
| Session cost | ctx.cost.total_cost_usd |
| Lines added/removed | ctx.cost.total_lines_added/removed |
| Session duration | ctx.cost.total_duration_ms |
| Git branch | git branch --show-current |
| Git dirty marker | git status --porcelain |
| Short SHA | git rev-parse --short HEAD |
| CI status | gh run list --branch <branch> |
| Open PR count | `gh pr list -q '. |
| Active subagents | parse transcript JSONL |
| MCP server count | parse claude mcp list --output json |
| Output style | ctx.output_style.name |
| Battery % (laptop) | pmset -g batt (macOS), /sys/class/power_supply/BAT0/capacity (Linux) |
| Net up/down | ping -c1 1.1.1.1 |
Common pitfalls
- Script not executable —
chmod +xthe script or the harness silently falls back to the default statusline. - Script slower than 1 second — the terminal feels laggy and the REPL appears to freeze; cache or precompute.
- Newlines in output — only the first line shows; double-check
printfvsecho. - Unicode width miscounted — emoji and CJK glyphs are 2 columns wide; align by character count if needed.
jqmissing on the host —jqis the most common way to parse stdin JSON; either ship a Python/Node script, orapt install jqas part of setup.stdinconsumed twice —INPUT=$(cat)is the safe pattern; laterjq <<< "$INPUT"works, but pipingcattwice does not.- Errors silently swallowed — the harness ignores non-zero exit codes from the statusline script; redirect stderr to a log file (
2>>~/.claude/statusline.log) to debug. - Project script with absolute path —
.claude/statusline.shis checked in; absolute paths in it (e.g.~/.cache/...) won't work on every contributor's machine. Prefer paths relative to$HOMEor tocwd.
Real-world recipes
Cost-aware statusline
Turn the cost segment red when it crosses a threshold.
#!/usr/bin/env bash
INPUT=$(cat)
COST=$(echo "$INPUT" | jq -r '.cost.total_cost_usd // 0')
RED=$'\033[31m'
GRN=$'\033[32m'
RESET=$'\033[0m'
if (( $(echo "$COST > 1.0" | bc -l) )); then
COLOR=$RED
else
COLOR=$GRN
fi
printf "${COLOR}\$%.2f${RESET}" "$COST"
Output:
$0.42
Last-action indicator
Read the last assistant tool call from the transcript JSONL and show it.
#!/usr/bin/env python3
import json, sys, pathlib
ctx = json.load(sys.stdin)
path = pathlib.Path(ctx["transcript_path"])
last_tool = "-"
if path.exists():
for line in reversed(path.read_text().splitlines()):
try:
ev = json.loads(line)
except Exception:
continue
if ev.get("type") == "assistant":
for c in ev.get("message", {}).get("content", []):
if c.get("type") == "tool_use":
last_tool = c["name"]
break
if last_tool != "-":
break
print(f" last:{last_tool}")
Output:
last:Edit
Plugin/skill HUD
Show how many available skills the session loaded.
#!/usr/bin/env bash
# Skills are listed in ~/.claude/skills/, .claude/skills/, and plugin folders.
COUNT=$(find ~/.claude/skills .claude/skills ~/.claude/plugins/*/skills -maxdepth 2 -name SKILL.md 2>/dev/null | wc -l | tr -d ' ')
printf "skills:%d" "$COUNT"
Output:
skills:12
Statusline as alarm
A subtle indicator that a Notification hook has fired since the last user input — pair with a Notification hook that touches a sentinel file.
#!/usr/bin/env bash
SENTINEL=/tmp/claude-notify
if [ -f "$SENTINEL" ]; then
printf "\033[33m! \033[0m"
fi
Output:
!
And in settings.json:
{
"hooks": {
"Notification": [
{"matcher": "", "hooks": [{"type": "command", "command": "touch /tmp/claude-notify"}]}
],
"UserPromptSubmit": [
{"matcher": "", "hooks": [{"type": "command", "command": "rm -f /tmp/claude-notify"}]}
]
}
}