claude-readout

A status line for Claude Code that renders correctly in the terminal it is
actually running in — including the one without a Nerd Font, and the one on a
light background — and can prove it.
◆ Opus 5 ~/…/a-fairly-long-project-name feature/a-descriptive-branch-name +3 ~7 ?2 !1 #428 │ ctx ████████▒▒▒▒▒▒ 62% $4.27 5h ██████▒▒ 78% →2h
Claude Code hands a JSON blob on stdin to whatever statusLine.command names
and prints what comes back. That is the whole contract. Everything interesting
is in what you do with a hundred columns — and in noticing that you may not
have a hundred columns, a Nerd Font, or a dark background.

Why another one
There are at least ten of these. Writing an eleventh because the tenth had
colours you didn't like is not a project. The wedge is narrower and it came out
of debugging a real status line on a real machine:
Status lines in this space tend to assume a dark terminal and a Nerd Font.
When either assumption is wrong they render broken, and the user cannot tell
why.
Two measured failures from that session, both now regression tests:
The font. The terminal was set to Monaco. Parsing Monaco's cmap table
directly shows it maps █ (U+2588) and ▒ (U+2592) and none of the
eighth-blocks ▏▎▍▌▋▊▉, and not ░ (U+2591). A progress bar built from █
plus ░ is therefore drawn from two different fonts at two different advance
widths, and comes out visibly jagged. Nothing tells you.
The background. iTerm2 profiles carry both a Background Color and a
Background Color (Dark). With macOS in Dark mode the second one is what is on
screen. A detector that reads the obvious key returns light for a terminal
whose real background is #14191E, and then paints near-black text onto
near-black. Meanwhile COLORFGBG=15;0 — published by the terminal at runtime —
was right the whole time.
So: runtime signals outrank configuration files, and glyph availability is a
property you measure rather than assume. claude-readout doctor does both and
shows its work; docs/CAPABILITIES.md is the resulting
terminal-and-font matrix.
|
claude-readout |
| Picks the palette from |
an OSC 11 query to the real terminal, then COLORFGBG, then the terminal's own config file, then dark |
| Picks the glyphs from |
the configured font's own cmap table, parsed directly — four tiers, unicode by default |
| Cost per render |
~2–3 ms per invocation, process start included; the render itself is well under a millisecond warm. Single static binary, no runtime |
| Runtime network access |
none, ever |
| Tells you why it chose |
doctor — evidence and consequence on every line |
The alternatives are worth knowing about, and some of them do things this one
does not. As of August 2026:
ccstatusline (~12.5k stars) has by
far the largest user base and an interactive configuration editor;
CCometixLine (~3.4k) is, like this
one, a compiled binary rather than a Node program; and
claude-powerline (~1.2k) does a
powerline look properly. Capability detection — measuring the font and probing
the background instead of assuming both — is what this one is for.
Install
v0.1.0 has not been tagged yet. Until it is, the Homebrew, curl and
Scoop/winget channels below have nothing to download — build from source with
go install. See RELEASING.md.
Homebrew:
brew install SteveVitali/tap/claude-readout
curl (macOS and Linux; downloads the release binary and verifies its
checksum):
curl -fsSL https://raw.githubusercontent.com/SteveVitali/claude-readout/main/install.sh | sh
Pass options through the pipe with sh -s --, e.g.
| sh -s -- --dry-run to see the plan without writing anything, or
| sh -s -- --prefix ~/bin.
Go:
go install github.com/SteveVitali/claude-readout/cmd/claude-readout@latest
Windows builds ship in the release (windows/amd64, windows/arm64) and the
Scoop and winget manifests are generated on every release run, but they are
not published yet — on Windows, download the .zip from the releases page
or use go install.
Quick start
Three commands, in the order they answer the three questions:
claude-readout init # measure this terminal, write the config, patch settings.json
claude-readout preview # see the result without restarting Claude Code
claude-readout doctor # what does this terminal actually do, and why
init is the only command that touches Claude Code's settings, and
init --dry-run tells you exactly what it would change first. See
What it touches for everything any command reads or writes.
Commands
| Command |
What it does |
render |
the status line itself: reads the payload on stdin, writes one line. The hot path |
init |
probe the terminal, cache the answers, write a config if there is none, and point statusLine.command at this binary. Idempotent; backs up settings.json first. --dry-run, --force, --json, --uninstall |
doctor |
the full diagnosis: Claude Code, terminal, font, config, render cost — each line with its evidence. --json for CI, --fix for the safe remediations, --payload to check a real payload against the schema |
preview |
draw the line from built-in sample sessions. --all-themes, --width N (repeatable), --glyphs TIER, --state NAME |
theme list | show | probe |
list palettes, show one as this terminal renders it, or run the OSC 11 query and cache the answer |
config path | edit | validate | dump |
where the config comes from, edit it, check it, or print it fully resolved |
version |
version, commit, build date. --json |
completion |
bash, zsh, fish or powershell completions |
Global flags: --config <file>, --no-color.
What it detects
Four independent axes. Each has a ladder, a cached answer and an explicit
override — detection is a default, never a cage. Anything you pin in config
wins over anything detected.
Background → light or dark palette. theme.appearance in config, then an
OSC 11 query to the terminal (cached for 24 hours, because render has no
terminal to ask), then COLORFGBG, then the terminal's own configuration file,
then dark.
Colour depth → truecolor / 256 / 16 / none. NO_COLOR first and absolutely,
then theme.color_depth, then COLORTERM, then TERM. Palettes are authored
once in truecolor and quantised down.
Glyph tier → what the font can actually draw. The terminal's configured font
is resolved, its cmap table is parsed directly, and the codepoints each tier
needs are tested. Cached for a week. If the font cannot be resolved the answer
is unicode, not ascii.
| Tier |
Meter |
Divider |
Needs |
ascii |
########=----- |
| |
nothing |
unicode (default) |
████████▒▒▒▒▒▒ |
│ |
U+2588, U+2592, U+2502 |
extended |
████████▋░░░░░ |
│ |
U+2588, U+2592, U+2502, U+258F…U+2589, U+2591 |
nerdfont |
as extended, plus branch/model/PR icons |
powerline |
U+2588, U+2592, U+2502, U+258F…U+2589, U+2591, U+E0A0, U+E0B0, U+E0B1, U+F219, U+F407 |
Width. layout.width, then CLAUDE_READOUT_WIDTH, then COLUMNS, then
ioctl(TIOCGWINSZ) on /dev/tty, then 100 — less Claude Code's padding and a
four-column safety margin. doctor reports which rung answered, because a
wrong width is the difference between an elegant line and a wrapped one.
Configuration
TOML at ${XDG_CONFIG_HOME:-~/.config}/claude-readout/config.toml. Everything
is optional and every default equals the behaviour you get with no file at all.
claude-readout config edit writes a fully-commented starter file the first
time; claude-readout config dump prints the resolved result.
preset = "default" # default | compact | minimal | powerline | ascii
[theme]
appearance = "auto" # auto | light | dark
palette = "primer"
color_depth = "auto" # auto | truecolor | 256 | 16 | none
glyphs = "auto" # auto | ascii | unicode | extended | nerdfont
[layout]
width = "auto" # "auto" or a column count
zone_divider = true
jitter_guard = true # pad numeric fields to their maximum width
[meter]
context_cells = 14
limit_cells = 8
warn_at = 60
danger_at = 85
style = "shade" # shade | solid | rule | none
# Segments are an ordered list; the order here is the order on the line.
# `rank` is sacrifice order under width pressure — higher goes first, and
# rank 0 is never dropped.
[[segment]]
kind = "model"
zone = "L"
rank = 0
show_context_size = true
[[segment]]
kind = "path"
zone = "L"
rank = 20
style = "leaf-emphasis"
max_width = 26
[[segment]]
kind = "context"
zone = "R"
rank = 0
label = "ctx"
# The escape hatch: anything the built-ins do not cover.
[[segment]]
kind = "custom"
zone = "R"
rank = 95
command = "tmux display-message -p '#S'"
timeout_ms = 50 # hard-killed, so a slow segment cannot
cache_ttl_s = 10 # stall the footer
Segment kinds: model, path, git, pr, context, cost, rate_limit,
renew, custom.
Precedence, lowest to highest: built-in defaults → the preset they name → your
config file → the project's .claude-readout.toml (searched upward from the
working directory) → the environment → flags. The four capability axes are the
one exception: a value pinned in config outranks the environment, because
detection is a default rather than a cage.
Environment overrides: CLAUDE_READOUT_CONFIG, CLAUDE_READOUT_PRESET,
CLAUDE_READOUT_THEME, CLAUDE_READOUT_PALETTE, CLAUDE_READOUT_COLOR_DEPTH,
CLAUDE_READOUT_GLYPHS, CLAUDE_READOUT_WIDTH,
CLAUDE_READOUT_ZONE_DIVIDER, CLAUDE_READOUT_JITTER_GUARD,
CLAUDE_READOUT_METER_STYLE. Plus NO_COLOR, which wins over all of them.
What it touches
MIT licensed, no telemetry, no analytics, no update check, and no network
access at runtime, ever — enforced by a test that fails if any package in the
binary's dependency graph is a network client. The one nuance: a custom
segment runs the command you put in your config, so that promise is about
what the binary does on its own account.
Reads, on the hot path — every render:
- the JSON payload on stdin, from Claude Code
${XDG_CONFIG_HOME:-~/.config}/claude-readout/config.toml, and
.claude-readout.toml from the working directory upward
${XDG_CACHE_HOME:-~/.cache}/claude-readout/ — the cached background and
glyph-tier answers
- the terminal's own configuration file, but only when the background cache
and
COLORFGBG have both missed: com.googlecode.iterm2.plist,
~/Library/Preferences/.GlobalPreferences.plist, kitty.conf,
ghostty/config, alacritty.toml, or Windows Terminal's settings.json,
whichever matches the terminal you are in
~/.claude.json, for the subscription renewal date behind the renew segment
- runs
git status --porcelain=v2 in the session's directory, cached for 3
seconds
Reads, only in the commands you run yourself — doctor, init, theme probe:
~/.claude/settings.json, to report or patch statusLine
- the resolved font file, to parse its
cmap table — plus one fc-match
subprocess on Linux. render never opens a font file; it reads the tier
these commands cached.
Writes, ever:
${XDG_CACHE_HOME:-~/.cache}/claude-readout/ — doctor, init and
theme probe write the background and tier they measure; render and the
git and custom readers write their own short-lived entries
${XDG_CONFIG_HOME:-~/.config}/claude-readout/config.toml — init,
config edit, and doctor --fix when it pins a key it recommends
~/.claude/settings.json — init only, backed up first, and only the
statusLine entry. Every other key, including every other key inside
statusLine, comes out exactly as it went in.
Caveats
Honest ones, because the pitch is honesty about capability:
render cannot ask the terminal anything. Claude Code spawns it without
a tty, so the OSC 11 background query and the font measurement happen in
doctor, init and theme probe, and render reads the cache they leave.
Run init once per terminal. doctor's own cold row will exceed the 10 ms
budget for exactly this reason — the budget is about the warm path, and the
report says so.
- tmux and screen may swallow the OSC 11 query, or answer it themselves
rather than passing it to the host terminal. The probe has a 250 ms timeout
and falls through to
COLORFGBG; doctor says when it is inside a
multiplexer and recommends pinning theme.appearance.
- Windows Terminal's colour scheme is not parsed. Its font is read from
settings.json, but resolving a scheme name through its theme tables is real
work that has not been done — and it answers OSC 11, which is a better signal
anyway.
- VS Code's
terminal.integrated.fontFamily is not read, so a VS Code
terminal falls back to the platform default font for the glyph measurement.
It answers OSC 11 for the background.
- WezTerm's
.wezterm.lua is not parsed — it is a program, not a config
file, and running a user's Lua to learn a colour is not a trade this makes.
WezTerm answers OSC 11.
doctor writes the caches it reports. That is the point of running it,
but it is a side effect worth stating. doctor --fix deliberately will not
touch Claude Code's settings.json: installing the status line is init's
job and it should be a thing you asked for.
preview does not run custom segments, on purpose — previewing a
configuration should not execute the commands in it.
- One contrast shortfall, recorded rather than waived. The light palette's
amber fill measures 2.46:1 against its own rendered track in truecolor and
2.19:1 at 256 colours, under the 2.5:1 the design rules set. It is inherited
from the reference implementation and pinned by the byte-identity corpus. It
is not dangerous — bar length encodes the value redundantly — but it is
real, and a unit test holds it at that measured value so it cannot quietly
get worse.
- At 256 colours and below, the dark palette's accent and its danger text
quantise to the same index. Authoring once and quantising down makes that
inevitable; boldness is what keeps them apart.
- On Windows,
doctor and init print filesystem paths with forward
slashes, including in --json finding values. That is deliberate: Git Bash
eats unquoted backslashes, so the statusLine.command init writes must use
forward slashes, and a report that tidied them away would be describing a
command that does not work.
Uninstall
claude-readout init --uninstall # remove the statusLine entry
That deletes the statusLine entry from ~/.claude/settings.json and leaves
every other key exactly as it was. It refuses if the status line configured is
not this program's — pass --force if you mean it anyway.
It does not put back a status line you had before installing — it removes
this one and leaves you to restore the other. settings.json.bak is written
once, the first time claude-readout touches the file, and is never
overwritten afterwards, so it always holds the configuration you had before
your first init. To put that configuration back:
cp ~/.claude/settings.json.bak ~/.claude/settings.json
--uninstall deliberately leaves your config file and the cache directory
alone — remove them too if you want nothing left:
rm -rf ~/.config/claude-readout ~/.cache/claude-readout
rm -f ~/.local/bin/claude-readout # or: brew uninstall claude-readout
Using as an Agent Skill
The repo doubles as a skill directory (SKILL.md), so Claude Code
can drive it conversationally:
ln -s /path/to/claude-readout ~/.claude/skills/claude-readout
Then "my status line looks broken" or "set up claude-readout" in any
conversation.
Development
go test ./... # includes the 112-case byte-identity corpus
go vet ./...
gofmt -l .
CI runs the suite on Linux, macOS and Windows — all three blocking — plus a
cross-compile of all six release targets, shellcheck, a p99 < 10 ms render
budget gate, and a full release rehearsal that publishes nothing.
See CONTRIBUTING.md for the walkthrough (including how to
add a segment), docs/ARCHITECTURE.md for the design and why each
decision went the way it did, docs/CAPABILITIES.md for
the terminal and font matrix, and RELEASING.md for the release
process.
A companion to claude-hibernate,
which hibernates and wakes Claude Code sessions across reboots.
Community project. Not affiliated with or endorsed by Anthropic. "Claude" is
Anthropic's trademark.
License
MIT