I Turned Claude Code’s Statusline Into A Two-Line Cockpit

Forty minutes into a session the other night I had a small, terrible thought: “how much money have I spent in here?”

I stared at the bottom of the terminal. The bottom of the terminal stared back. There was a chip telling me which model I was on, a sleepy little bypass permissions on indicator, and then… a void. A great expanse of beige nothing where, in any sane piece of tooling, a dashboard would live. Forty minutes of asking Opus 4.7 to do increasingly elaborate things and I had no idea whether I’d burned a dime, a dollar, or — more realistically — several dollars I was already pretending not to think about.

This is the part where a normal person would type /cost, eyeball the number, sigh, and move on. I instead did what I always do when a piece of software refuses to tell me something I want to know on a permanent, always-visible, peripheral-vision basis: I went looking for the config.

The Strip Of Real Estate Nobody Configures

Claude Code has a setting called statusLine. It runs a full shell script. Every assistant turn. And — this is the part that made me snort-laugh into my keyboard — it pipes the entire session state to that script as JSON on stdin. Not “current model and directory.” The entire session state.

Cost in USD. Context window usage. Rate-limit percentages for the 5-hour and 7-day rolling windows on your Pro/Max subscription. Git worktree status. Vim mode. Current reasoning effort. Agent name. Output style. Lines added and removed this session. The current model’s display name and its model ID. The total wall-clock time, and the time spent waiting on the API as separate fields. It’s all just sitting there, waiting for jq to come pick it up and turn it into a row of text at the bottom of your terminal.

The default statusline shows none of this. The default statusline shows the model name and a permission chip and otherwise behaves like it just got out of bed. You’re supposed to write your own. The docs are right there, on a page that I’m fairly sure has fewer human readers than the average company’s privacy policy.

So obviously I built a cockpit.

Anatomy Of A Two-Line Brain Dashboard

The version I shipped is two lines. Line one is identity: which model is currently lighting my money on fire, where I am on the filesystem, which git branch I’m on. Line two is pressure and economics: a color-coded context bar, dollars spent, wall-clock timer, and rate-limit usage.

Here’s what it looks like, taken straight off my screen this morning while I was poking at a different bash script:

[Opus 4.7 (1M context)] 📁 ~/Code/mkdown | 🌿 main
██░░░░░░░░  7%  |  $1.62  |  ⏱ 22m 30s  |  5h:0% 7d:0%
‣‣ bypass permissions on (shift+tab to cycle)

(That third row isn’t mine — it’s the harness’s own permission-mode chip. The whole point of a statusline is that it composes with the existing UI, not on top of it. Notifications, MCP errors, the context-low warning, the bypass-permissions toggle — they all keep doing their thing in the same row, undisturbed.)

A few opinionated choices baked into the script:

  • The bar is colored by pressure. Green under 70%, yellow 70–89%, red 90%+. I want my peripheral vision to know I’m cooked before my conscious mind does.
  • No git dirty-count. I tried +staged ~modified indicators for about a day. Turns out I do not care, at-a-glance, how many files I have in flight. I care which branch I’m on. The dirty-count is what git status is for.
  • Branch is read live, no caching. git branch --show-current is fast enough in any repo I work in that the 5-second /tmp cache the docs gently suggest for big monorepos felt like premature engineering for my use case.
  • Path is full, with $HOME collapsed to ~. Just the basename — the most common pattern in the example scripts — lies to me about which src/ I’m in. I have approximately eight thousand src/ directories. Show me the path.
  • Rate limits drop out gracefully if you’re on a plan that doesn’t expose them. jq -r '.rate_limits.five_hour.used_percentage // empty' is the magic word. Don’t print a field that doesn’t exist; let the row stay clean.

The script itself is maybe seventy lines of bash, most of which is ANSI escape codes and arithmetic for the progress bar. The interesting work happens in three jq invocations and one git rev-parse. It is, by every measure, embarrassingly easy for the amount of useful information it surfaces.

The Field That Made Me Sweat

You can ignore most of the JSON Claude Code throws at you. You can ignore vim mode, the worktree object, the agent name, the lines-added counter, the session UUID. Most of it is texture.

There is one field you should not ignore, and it is the one I now stare at every single turn:

cost.total_cost_usd

This field has changed the way I work. Not because I’m cheap — though I am — but because a meter you can see is a meter you negotiate with. Forty minutes of free-associating with Opus 4.7 used to feel weightless. Now the dollar sign on line two is right there, ticking up, like a taxi meter in the corner of my eye. I have noticeably fewer “let’s just try one more big refactor” moments after 11pm. I let subagents do longer chunks of work because the cost-per-decision is now legible to me, and I can see when I’m being silly by hand-holding something a Haiku could one-shot.

This is the bit they don’t tell you when they hide it in a docs page nobody reads. The statusline isn’t decoration. It’s a tiny behavioral nudge factory wedged into one line of your terminal.

The Bigger Point, Hiding In A 70-Line Bash Script

The Claude Code harness is wildly more configurable than it lets on. The default UX is fine, in the way that the default everything is fine. But the people building it deliberately left a bunch of small holes unfilled — statusline, hooks, custom slash commands, output styles, agent definitions, MCP servers — and they piped enough raw session state into those holes that you can build genuinely useful instrumentation in an afternoon.

Most people I know who use Claude Code daily have configured none of this. They open the app, they accept the default surface, and then they complain (lovingly, but constantly) that the tool is opaque. The tool is not opaque. The tool is a buffet of escape hatches that nobody picks up a plate at.

The statusline is the easiest one to start with because the contract is comically simple — “we pipe you JSON, you print some text” — and the feedback loop is immediate. You see the result in your own terminal within the next turn. You can tweak the colors, swap the icons, drop fields you don’t care about, add fields nobody else would want, and the whole thing lives in a single shell script in ~/.claude/statusline.sh. Nobody is reviewing your PR. Nobody is testing your bar character. The only user that matters, as always, is the weirdo in the mirror.

Shipping Your Own Cockpit

If you want the version I’m running — and yes, I wrote an entire post about a script and then, with all the grace of a man strolling face-first into a glass door, forgot to actually include the script — here is the whole thing. It also lives as a GitHub gist if you’d rather just copy-paste it:

 1#!/bin/bash
 2# ~/.claude/statusline.sh
 3# Two-line Claude Code statusline.
 4#
 5# Role in system:
 6#   Invoked by Claude Code after each assistant message / on permission-mode or
 7#   vim-mode changes (debounced 300ms). Receives JSON session data on stdin
 8#   and prints two lines of formatted output. Coexists with the built-in
 9#   right-side notification area (MCP errors, context-low warnings, etc.) —
10#   those are NOT rendered by this script, they share the row by design.
11#
12# Wired up from:  ~/.claude/settings.json -> "statusLine.command"
13# Related docs:   https://code.claude.com/docs/en/statusline
14#
15# Layout:
16#   line 1: [Model] dir | branch
17#   line 2: <context-bar> N% | $cost | mm:ss | 5h:N% 7d:N%
18#
19# Design notes:
20#   - No git status / dirty-check on purpose (user pref) — only `branch --show-current`.
21#   - jq is required. If absent the script silently degrades to "[unknown]".
22#   - Rate limits are absent for non-Pro/Max users; handled with `// empty`.
23
24input=$(cat)
25
26# Bail to a useful default if jq is missing rather than printing nothing.
27if ! command -v jq >/dev/null 2>&1; then
28  echo "[statusline: install jq]"
29  exit 0
30fi
31
32MODEL=$(echo "$input" | jq -r '.model.display_name // "unknown"')
33DIR=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // ""')
34PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
35COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
36DURATION_MS=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')
37FIVE_H=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
38WEEK=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
39
40CYAN='\033[36m'
41GREEN='\033[32m'
42YELLOW='\033[33m'
43RED='\033[31m'
44DIM='\033[2m'
45RESET='\033[0m'
46
47# Color the context bar by pressure so high usage is visually loud.
48if [ "$PCT" -ge 90 ]; then
49  BAR_COLOR="$RED"
50elif [ "$PCT" -ge 70 ]; then
51  BAR_COLOR="$YELLOW"
52else
53  BAR_COLOR="$GREEN"
54fi
55
56BAR_WIDTH=10
57FILLED=$((PCT * BAR_WIDTH / 100))
58[ "$FILLED" -gt "$BAR_WIDTH" ] && FILLED=$BAR_WIDTH
59EMPTY=$((BAR_WIDTH - FILLED))
60BAR=""
61[ "$FILLED" -gt 0 ] && printf -v FILL "%${FILLED}s" && BAR="${FILL// /█}"
62[ "$EMPTY" -gt 0 ] && printf -v PAD "%${EMPTY}s" && BAR="${BAR}${PAD// /░}"
63
64MINS=$((DURATION_MS / 60000))
65SECS=$(((DURATION_MS % 60000) / 1000))
66DUR_FMT=$(printf '%dm %02ds' "$MINS" "$SECS")
67COST_FMT=$(printf '$%.2f' "$COST")
68
69# Branch is cheap — no caching needed. Silent fail outside a repo.
70BRANCH=""
71if git rev-parse --git-dir >/dev/null 2>&1; then
72  B=$(git branch --show-current 2>/dev/null)
73  [ -n "$B" ] && BRANCH=" | 🌿 $B"
74fi
75
76# Rate-limits segment is omitted entirely for non-subscribers.
77LIMITS=""
78if [ -n "$FIVE_H" ]; then
79  LIMITS=$(printf '5h:%.0f%%' "$FIVE_H")
80fi
81if [ -n "$WEEK" ]; then
82  W=$(printf '7d:%.0f%%' "$WEEK")
83  LIMITS="${LIMITS:+$LIMITS }$W"
84fi
85
86# Collapse $HOME to ~ for readability without losing path context.
87DIR_DISPLAY="$DIR"
88case "$DIR" in
89  "$HOME") DIR_DISPLAY="~" ;;
90  "$HOME"/*) DIR_DISPLAY="~${DIR#$HOME}" ;;
91esac
92
93# Line 1: identity + location
94printf '%b\n' "${CYAN}[$MODEL]${RESET} 📁 ${DIR_DISPLAY}${BRANCH}"
95
96# Line 2: pressure + economics
97LINE2="${BAR_COLOR}${BAR}${RESET} ${PCT}% | ${YELLOW}${COST_FMT}${RESET} | ${DIM}${DUR_FMT}${RESET}"
98[ -n "$LIMITS" ] && LINE2="${LINE2} | ${DIM}${LIMITS}${RESET}"
99printf '%b\n' "$LINE2"

Then wire it up — the shape is roughly:

  1. Drop that script at ~/.claude/statusline.sh (it reads JSON from stdin and prints two lines).

  2. Wire it into ~/.claude/settings.json:

    1{
    2  "statusLine": {
    3    "type": "command",
    4    "command": "~/.claude/statusline.sh",
    5    "padding": 1
    6  }
    7}
    
  3. chmod +x ~/.claude/statusline.sh and trigger any assistant turn. The line appears.

The full reference is at code.claude.com/docs/en/statusline — schema, all the field names, copy-pasteable examples in bash, Python, and Node. It’s genuinely a good docs page. It’s also, judging by how few people I see with custom statuslines, read by nobody.

Better still: just paste the docs page into your own Claude Code session and ask it to set the whole thing up. The tool can configure itself. It’s a magnificent little ouroboros and I love it.

Build the cockpit. Watch the dollars tick. Notice that you suddenly have opinions about the work you’re handing to the model, because for the first time you can actually see what the work is costing you.

And if you build a wilder one than mine — a sparkline of context usage over time, an emoji that changes based on burn rate, a hex string showing your session ID for sharing transcripts, a little flame that gets bigger as your 5-hour quota depletes — please tell me. I will absolutely steal it.


Yes, the LLM helped me write this post and configure the statusline I’m describing in this post. The statusline that watches the LLM. The LLM that wrote the post about the statusline that watches the LLM. We have officially reached the part of the timeline where every tool watches every other tool, and I, the human at the keyboard, am mostly here for the vibes. Hell yeah.