tempo / notes / claude code statusline

guide

Building a Claude Code statusline worth looking at

The statusline is the only piece of persistent screen real estate you get in Claude Code. It is one line, it re-renders constantly, and most people either leave it empty or fill it with things they never actually read. Here is how it works and what earns a place on it.

The mechanism, in one paragraph

Claude Code runs a command you nominate, hands it a JSON blob describing the session on stdin, and prints the first line of stdout as your statusline. That is the entire contract. Your command can be a shell one-liner, a Python script, a compiled binary — anything your shell can execute. It re-runs as the session changes, throttled, so it needs to be fast.

You register it in ~/.claude/settings.json (user-wide) or a project's .claude/settings.json:

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

There is also a /statusline slash command inside Claude Code that will set one up for you conversationally. Useful for a first pass; the file is where you live afterwards.

Look at your own payload before you write anything

Every guide, including this one, will show you a payload shape. The fields available differ between Claude Code versions, so the only shape worth trusting is the one your install actually sends. Spend the first thirty seconds capturing it:

#!/usr/bin/env bash
# ~/.claude/statusline.sh — inspection pass
cat > /tmp/statusline-payload.json
echo "payload captured"

Make it executable, register it, open a session, then read /tmp/statusline-payload.json. You will typically find something along these lines:

{
  "session_id": "…",
  "transcript_path": "/Users/you/.claude/projects/…/….jsonl",
  "cwd": "/Users/you/projects/app",
  "model": { "id": "…", "display_name": "Opus" },
  "workspace": { "current_dir": "…", "project_dir": "…" },
  "version": "…",
  "output_style": { "name": "default" }
}

Depending on version you may also get cost and duration figures for the session. Treat every field as optional in code — read defensively, never index blindly — because a statusline that throws on an unexpected payload is a statusline that vanishes.

A working starting point

Directory, branch, and model. Nothing clever, and about as much as you can read at a glance without actually reading:

#!/usr/bin/env bash
payload=$(cat)
dir=$(printf '%s' "$payload" | python3 -c 'import json,sys,os; \
  print(os.path.basename(json.load(sys.stdin).get("cwd","")))' 2>/dev/null)
model=$(printf '%s' "$payload" | python3 -c 'import json,sys; \
  print(json.load(sys.stdin).get("model",{}).get("display_name",""))' 2>/dev/null)
branch=$(git branch --show-current 2>/dev/null)

printf '%s' "${dir:-~}"
[ -n "$branch" ] && printf ' \033[2m(%s)\033[0m' "$branch"
[ -n "$model" ] && printf ' \033[2;36m%s\033[0m' "$model"
echo

ANSI colour codes work and are the main tool you have for hierarchy. Dim (\033[2m) is the most underused: it lets you keep information available without it competing for attention. Emoji render, but they cost width and read as noise once there are more than one or two.

What actually earns a place on one line

The useful filter is not "is this interesting" but "would I change what I am doing because of this?" A statusline is an instrument panel, not a dashboard. Most things people put on it fail that test.

CandidateVerdict
Working directoryYes. Cheap insurance against running the right command in the wrong repo.
Git branchYes, for the same reason, and it catches the detached-HEAD surprise early.
ModelYes if you switch models. Otherwise it is decoration.
Dirty-tree markerOften. One character is enough.
Session costDepends. Genuinely useful on a metered plan; for most people it converts a working session into a slot machine.
ClockNo. Your operating system already did this, better.
Token countsRarely. You cannot act on it, and watching it is its own distraction.
How long you have been going, and how that is landingYes — and almost nobody has it. See below.

Composing, rather than replacing

There is exactly one statusLine slot, which means every tool that wants a statusline is competing for it. The polite pattern — and the one to demand of anything you install — is wrapping: store the user's original command, run it first, print its output verbatim, then append your own segment.

#!/usr/bin/env bash
payload=$(cat)

# Run the statusline that was here before, unmodified, with its own payload.
if [ -f ~/.mytool/wrapped.json ]; then
  prev=$(python3 -c 'import json; print(json.load(open("'"$HOME"'/.mytool/wrapped.json"))["command"])')
  printf '%s' "$(printf '%s' "$payload" | timeout 3 sh -c "$prev" | head -1)"
  printf ' '
fi

printf 'mytool ●\n'

This is what Tempo's installer does. If you already have a statusline it is neither discarded nor silently skipped: your command is recorded in ~/.tempo/wrapped.json and runs first, with Tempo's glyph appended after it. Deleting that file returns you to Tempo alone; putting your own command back in settings.json drops Tempo entirely.

Back up settings.json before any tool touches it — and prefer tools that do it for you, with a timestamp. Merging JSON into a config a human hand-edits is exactly the operation that eats a config.

Four rules that keep a statusline from being a liability

  1. Always exit 0. Every path, including the ones you did not think about. A status command is not a place to surface errors.
  2. Be fast. It runs constantly. Anything that can block — a network call, a cold interpreter, an unbounded git command in a huge repo — needs a timeout and a fallback. Read cached state from a file rather than computing it.
  3. Print one line. Only the first is used, so extra output is silently discarded — which makes stray debug prints genuinely confusing to chase.
  4. Render something, always. The single most common bug report for any statusline tool is "it isn't doing anything", and it is almost always a state file that went missing while the tool pretended everything was fine. A dim glyph that means "no data" is far better than a blank space.

The segment nobody has

Everything above is context about the code. None of it is context about you — and after four hours in an agentic session, you are the component with the least headroom. Directory and branch will not tell you that you have been going for six hours, that your last three prompts collapsed into fragments, or that this is the fourth day in a row without a lighter one.

That is the segment Tempo adds: a glyph that warms as session load rises — steady, elevated, depleted — and, past an hour of real engagement, the running total for the day alongside it. It reads the shape of the session, never its words, and stores no prompt text, file contents, paths or commands. If it earns the top of the scale it will offer one 40-second reset in a seam, at most twice a day. Why volume matters as much as trouble is covered in why hour six feels different.

Add the glyph in two minutes

It wraps an existing statusline rather than replacing it, and backs up your Claude settings with a timestamp before it merges anything.

curl -fsSL https://trytempo.net/install.sh | bash

Piping to bash deserves a look first — read the script, or download the zip and install by hand.

free in betapython 3no network calls