AboutExperienceBlogContact
LinkedInGitHubGitLabWhatsApp
All posts

4 min read

The Missing Gauge

claude-codepythontoolingdeveloper-experience

Long Claude Code sessions tend to end the same way. The context window fills, auto-compact fires, and the model comes back having lost half of what you agreed on. It starts re-deciding things you settled two hours ago.

None of it has to be a surprise. Claude Code computes how full the window is on every render. It just never draws the number.

A status line fixes that. It is one line printed under the prompt, produced by a command you choose, showing whatever you decide matters. Mine shows six things.

zsh

Edit(statusline.py)

Updated 4 files · +312 -88

>Try "add a status line"
Haiku 4.5168% free2⎇ feat/status-line*3+312 -88464k/200k5my-project6
  1. 1ModelWhich model is actually answering. Useful when you run several sessions with different models.
  2. 2Context leftHeadroom before auto-compact. This is the one that decides whether the session survives.
  3. 3BranchCurrent git branch. The asterisk means the working tree is dirty.
  4. 4ChurnLines the agent added and removed. The size of the diff you owe a review.
  5. 5TokensContext used against the window. The 1M tier is detected at runtime, never assumed.
  6. 6ProjectProject directory name, so four terminal tabs never get mistaken for one another.
Hover any segment or its note. The line and the legend are the same six facts, twice.

The second segment is the reason the other five exist.

The contract is four lines wide

You give Claude Code a command. Before every render it pipes a JSON snapshot of the session to stdin, and whatever the command prints to stdout becomes the status line. That is the entire API, which means the whole thing is a filter:

import json, sys

data = json.load(sys.stdin)
print(data["model"]["display_name"])

Point settings.json at it and you have a working status line:

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

Everything past this point is deciding what deserves the space.

The one part that isn't obvious

Token usage lives in the transcript, a JSONL file with one message per line. The tempting move is to sum input_tokens across every line. The result climbs fast, looks plausible, and is badly inflated.

Here is why. The model has no memory between turns, so every turn re-sends the whole conversation as input. Each turn's usage therefore already describes everything sent so far: if turn one cost 10k tokens and turn two adds 2k, turn two reports 12k. Sum the lines and you get 22k for a conversation that occupies 12k. Prompt caching only splits the report into three buckets: fresh tokens, tokens re-read from cache, tokens just written to cache. The three buckets of the newest message add up to exactly what sits in the window right now:

for line in reversed(lines):
    usage = (json.loads(line).get("message") or {}).get("usage")
    if usage:
        return (usage["input_tokens"]
                + usage["cache_read_input_tokens"]
                + usage["cache_creation_input_tokens"])

Walk backwards, take the first line with usage, add the three fields, stop.

The bar also needs a denominator: the capacity of the window, which the session JSON never states outright. Most models run 200k, some run 1M, so the script checks two clues and takes either: [1m] in the model id, or the exceeds_200k_tokens flag in the payload. Guess wrong on a 1M session and the bar reads empty at 200k while 800k of room remains. A bar that lies about its own scale is worse than no bar.

Three rules that keep it boring

The script runs on every render, inside the terminal you are trying to work in. That is a hostile place to be clever, so it follows three rules.

Never raise. A status line that throws replaces six useful facts with a stack trace, over and over. Every risky call is wrapped; a malformed payload degrades to printing Claude Code and moving on.

Never hang. Every git call carries timeout=0.25. A slow network mount costs a quarter of a second, not a frozen prompt.

Never need installing. Standard library only. One file, curl it, done.

What changed

Compaction stopped happening to me. When the bar drops past two cells I finish the thought, commit, and start a fresh session with the context I chose to keep, instead of whatever survived an automatic summary at the worst possible moment.

All of it in under a hundred lines of Python that pay for themselves every session.


The script and install steps are in the repo: status-line. One file, four lines of config, done.