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.

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

Restart claude (or run /config then save) and the new statusline takes over.

bash
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.

json
{
  "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

FieldTypeDescription
session_idstringOpaque session identifier (sess_01...)
transcript_pathstringAbsolute path to the session JSONL transcript
cwdstringCurrent working directory
model.idstringActive model ID (e.g. claude-sonnet-4-6)
model.display_namestringHuman-readable model name
workspace.current_dirstringSame as cwd (kept for plugin compatibility)
workspace.project_dirstringProject root if Claude detected one
versionstringclaude-code binary version
output_style.namestringCurrent output style (default, concise, ...)
cost.total_cost_usdnumberCumulative session cost in USD
cost.total_lines_addednumberLines added across the session
cost.total_lines_removednumberLines removed across the session
cost.total_api_duration_msnumberTime spent waiting on the API
cost.total_duration_msnumberWall-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.

bash
#!/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:

text
  Sonnet 4.6  myproject

Richer bash example

Adds git branch, last commit SHA, and session cost.

bash
#!/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:

text
  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.

python
#!/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:

text
  Sonnet 4.6  myproject  main*@a3f12c  $0.12

Then wire it up:

json
{
  "statusLine": {
    "type": "command",
    "command": "python3 ~/.claude/statusline.py"
  }
}

Node example

For projects already invested in Node, a JS statusline is one require away.

javascript
#!/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:

text
  Sonnet 4.6  myproject  main*@a3f12c  $0.12
json
{
  "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.

bash
#!/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:

text
Sonnet 4.6 in /home/alice/Code/myproject

Respect NO_COLOR=1 for users who disable color:

bash
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.

bash
# 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:

text
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.

TriggerFrequency
Conversation turn endsAlways
Tool call completesAlways
/compact, /clear, /modelAlways
Background tick~5s when idle
User keystrokeNever (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.

json
{
  "statusLine": {
    "type": "command",
    "command": ".claude/statusline.sh"
  }
}
bash
#!/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:

text
  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.

bash
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:

text
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.

python
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.

IndicatorSource
Modelctx.model.display_name
Session costctx.cost.total_cost_usd
Lines added/removedctx.cost.total_lines_added/removed
Session durationctx.cost.total_duration_ms
Git branchgit branch --show-current
Git dirty markergit status --porcelain
Short SHAgit rev-parse --short HEAD
CI statusgh run list --branch <branch>
Open PR count`gh pr list -q '.
Active subagentsparse transcript JSONL
MCP server countparse claude mcp list --output json
Output stylectx.output_style.name
Battery % (laptop)pmset -g batt (macOS), /sys/class/power_supply/BAT0/capacity (Linux)
Net up/downping -c1 1.1.1.1

Common pitfalls

  1. Script not executablechmod +x the script or the harness silently falls back to the default statusline.
  2. Script slower than 1 second — the terminal feels laggy and the REPL appears to freeze; cache or precompute.
  3. Newlines in output — only the first line shows; double-check printf vs echo.
  4. Unicode width miscounted — emoji and CJK glyphs are 2 columns wide; align by character count if needed.
  5. jq missing on the hostjq is the most common way to parse stdin JSON; either ship a Python/Node script, or apt install jq as part of setup.
  6. stdin consumed twiceINPUT=$(cat) is the safe pattern; later jq <<< "$INPUT" works, but piping cat twice does not.
  7. 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.
  8. Project script with absolute path.claude/statusline.sh is checked in; absolute paths in it (e.g. ~/.cache/...) won't work on every contributor's machine. Prefer paths relative to $HOME or to cwd.

Real-world recipes

Cost-aware statusline

Turn the cost segment red when it crosses a threshold.

bash
#!/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:

text
$0.42

Last-action indicator

Read the last assistant tool call from the transcript JSONL and show it.

python
#!/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:

text
  last:Edit

Plugin/skill HUD

Show how many available skills the session loaded.

bash
#!/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:

text
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.

bash
#!/usr/bin/env bash
SENTINEL=/tmp/claude-notify
if [ -f "$SENTINEL" ]; then
  printf "\033[33m! \033[0m"
fi

Output:

text
! 

And in settings.json:

json
{
  "hooks": {
    "Notification": [
      {"matcher": "", "hooks": [{"type": "command", "command": "touch /tmp/claude-notify"}]}
    ],
    "UserPromptSubmit": [
      {"matcher": "", "hooks": [{"type": "command", "command": "rm -f /tmp/claude-notify"}]}
    ]
  }
}