weaverbird
A statusline multiplexer for Claude Code. Claude Code exposes exactly one statusLine.command.
weaverbird is that command. It reads the session JSON once, gathers structured records from many
independent provider programs that do not know about each other, and weaves them into one status bar,
owning color, ordering, separators, width degradation, and staleness itself.
Providers emit structured data and a semantic class, never pre-styled ANSI, so a bar assembled from
tools that were never coordinated still reads as one coherent, themeable system.
The problem it solves
Claude Code allows a single statusline command, so tools that each want to be on the bar cannot
coexist. The author has three that do: bloodhound (subscription quota), usage-gov (pacing alarm),
and pigeon (inter-session messages). Only one can own the slot, and the other two go dark.
weaverbird owns the slot once and composes all of them, plus anything else you drop in.
How it works
A provider publishes two things, the way a REST service publishes an OpenAPI document and endpoints:
- a spec: a small capability document declaring the widgets it offers and their static metadata
(id, kind, priority, icon, cache policy);
- value records: one JSON line per widget with the current value, produced on demand.
Every widget declares a kind (text, meter, timestamp, or series) and the kind decides
which further fields it carries: a text widget hands over an opaque string; a meter hands over a
percentage and lets weaverbird compose and theme the bar; a timestamp hands over an instant and lets
the host format and re-render elapsed time without asking the provider again; a series hands over a
run of numbers and weaverbird draws the sparkline. A provider computes its own severity and sends it as
class; weaverbird never derives one from a threshold.
weaverbird reads the spec once, then fetches values per render, in parallel, with a per-provider
timeout and a last-known-good cache, and it only re-invokes a provider when that provider's declared
cache policy says something changed. It maps class to color from one theme, lays the widgets out,
and degrades gracefully when the terminal is narrow: a cheapest-move-first cascade shrinks a bar,
swaps a text to its short form, drops icons, truncates, decomposes a meter's parts, and only then
drops a widget outright.
Vocabulary: provider (a program or file that supplies data), widget (one addressable unit,
identified by an id like bloodhound.week), kind (what a widget's data is), spec (a
provider's capability document), value record (one widget's current value).
Install
weaverbird is a single Go binary.
make install # builds and installs the weaverbird binary
weaverbird install # claims the statusLine slot in ~/.claude/settings.json (backs it up first)
weaverbird install also creates ~/.claude/weaverbird/providers/ and sets a refreshInterval so
that time-based and alarm providers stay live while a session idles. Run weaverbird doctor at any
time to confirm the slot is still owned by weaverbird and to see the discovered providers and their
health.
Adding a provider
Drop a file into ~/.claude/weaverbird/providers/. That is the entire registration; no shared config
is ever edited. An entry is one of:
- a descriptor
NN-name.provider.json that declares how to fetch the spec and values (an exec
command you choose, a frozen file, or an inline document);
- a bare executable that reads the session JSON on stdin and prints value records, for a quick
script;
- a
*.ndjson file that some daemon writes on its own cadence.
The last two need nothing else: every widget id they mention gets a default declaration, a text
widget re-read every render. To declare more than that (a meter, a cache policy, a second row), drop
a spec document next to the entry, named for it: 50-clock and 50-clock.spec.json.
Reach for a descriptor when the provider is a tool you installed rather than a script you wrote. You
have to name the command (bloodhound weaverbird value is only expressible there), and the spec then
ships and upgrades with the tool instead of living in your config.
Disable one with weaverbird disable <name> (it appends .disabled); re-enable with enable.
A widget can be made conditional from config.json, alongside layout. This is yours to set, not a
provider's: a provider says what it can show, layout says what you want shown, and when says under
what circumstances.
{
"layout": ["bloodhound.5h", "context.usage", "vcs.branch", "model.name"],
"when": {
"vcs.branch": { "unless": { "env": { "TERM_PROGRAM": "WarpTerminal" } } }
}
}
That hides the git branch inside Warp, which already shows it, and keeps it everywhere else. A rule is
if or unless over an env map, a session map (model, cwd, project_dir, and friends), or
both. Values are globs, lists of globs, or true/false for "is it set".
A hidden widget is not merely invisible, it is never fetched: its provider is not asked for it, and if
nothing else of that provider's is wanted, the provider is not run at all. weaverbird doctor prints
every rule, the values it read, and whether the widget is shown or hidden.
The full contract is SPEC.md. A cookbook of every form in shell, Python, and Go is EXAMPLES.md.
Machine-readable JSON Schemas are in schema/.
Writing a provider in Go
Import the provider helper package (pure standard library, no cobra required). It carries the wire
types and lifts the whole subcommand into one call:
import wb "github.com/PeterSR/claude-code-weaverbird/provider"
var spec = wb.Spec{V: 1, Provider: "bloodhound", Icon: "🩸", Widgets: []wb.Widget{
{ID: "bloodhound.week", Title: "Weekly quota", Kind: wb.KindText, Priority: 20,
Text: &wb.TextKind{}, Cache: &wb.Cache{TTLSec: 10}},
}}
func value(s wb.Session, requested []string) ([]wb.Value, error) {
// reuse your tool's existing data; compute your own class (no thresholds
// to hand over); return one record per widget, omit to stay silent
return []wb.Value{
{ID: "bloodhound.week", FullText: "wk 38%", ShortText: "38%", Class: wb.ClassOK},
}, nil
}
// inside your tool's `weaverbird` subcommand:
// return wb.Dispatch(args, os.Stdin, os.Stdout, spec, value)
See EXAMPLES.md section 9 for the full subcommand.
Providers are not bundled
weaverbird ships no descriptors. A descriptor only declares how to reach a provider (the command to
run for its spec and for its values) and weaverbird fetches the spec once per session and caches it,
so there is nothing worth freezing into this repo. Registration lives on the machine that has the
tools installed: drop a file into ~/.claude/weaverbird/providers/ (section 2 of SPEC.md) and that
is the whole of it.
weaverbird depends on no particular provider. It is equally happy with one, with several, or with
providers you write yourself.
The ambient widgets this README's examples use (vcs.branch, context.usage, model.name, and
friends) come from weaverbird-contrib,
a companion module: one binary carrying git, context, session, model, toolchain, kube, and container
providers, registered with a single weaverbird-contrib install.
Theming
Color comes from a theme: a name plus a light and a dark palette. A palette is a flat map from key
to color: classes color a widget's text; tones (<class>.tint, <class>.ink) are a lighter
fill and a readable ink for that class; bar colors (bar.track, bar.ink, bar.fill.<class>)
color a meter's bar when it renders as a track-style gauge. Any color value may be a reference to
another key in the same palette, written @key, so a theme states a color once and reuses it.
The classes lie on two axes. ok, info, warn, danger, stale and neutral are severity,
how alarming a reading is. gain and loss are polarity, which direction a reading moved: lines
deleted are a loss, not a danger. A provider still never names a color, so green-for-more stays one
theme's rendering of gain rather than something a provider hardcodes, and a theme that maps neither
polarity key renders both as plain neutral.
The split matters for more than accuracy. "Color is reserved for what wants attention" is a rule about
the severity axis, where the classes compete for one channel and coloring the calm ones is what
devalues the alarming ones. Polarity makes no bid on that channel, so a restrained theme can spend
color there freely. Two themes ship in the binary: vanilla (the default: classic ANSI yellow and red
for warn and danger, dim for stale, nothing at all for ok, info and neutral, and green and
red for gain and loss, which is that policy split in practice) and teal (a truecolor theme
committing to hues for every class, including neutral, and keeping polarity deliberately less
saturated than severity). Both declare bar colors for every class, so both can render a track-style
gauge. Drop your own at ~/.claude/weaverbird/themes/<name>.json:
{
"name": "sunset",
"light": {
"warn": "#e2892a", "warn.tint": "#f6d9a8",
"danger": "#d1483a", "danger.tint": "#f0b3ac",
"bar.track": "#e8e8e8", "bar.ink": "#1c1c1c",
"bar.fill.warn": "@warn.tint", "bar.fill.danger": "@danger.tint"
},
"dark": { "warn": "#f0972f", "danger": "#e2564a", "bar.track": "#303030", "bar.ink": "#f0f0f0" },
"separator": " | ",
"provider_separator": " -- ",
"bar": { "style": "track", "fill": "▓", "empty": "░" }
}
A key's value is "@key" (a reference, resolved against the same palette and appearance), "#rrggbb"
(truecolor), a 256-color index, or a named ANSI color; a user theme of the same name overrides a
built-in. A reference to a missing key, or a cycle, resolves as uncolored rather than erroring or
hanging. separator and provider_separator are both optional, the plain text rendered, dim, between
widgets, exactly as before. The theme-level bar object sets the bar's glyphs (fill/empty/
start/end, glyph style only) and the theme's own preferred style: glyph (default) draws
fill/empty characters colored in the foreground; track draws the bar as a colored background so parts
placed inside it sit on top of it, but track is only reachable when color is on and the active
palette actually declares bar.* colors, else it falls back to glyph.
Which theme, appearance, and bar style are active is a pointer in ~/.claude/weaverbird/config.json
(WEAVERBIRD_THEME/WEAVERBIRD_APPEARANCE override the first two for one process), changed with
weaverbird theme use <name>, weaverbird theme appearance <auto|light|dark>, and
weaverbird theme bar <glyph|track>, never by hand-editing colors. auto appearance resolves to light
or dark from, in order: the env override, the config pointer, Claude Code's own
~/.claude/settings.json theme, $COLORFGBG, else dark. weaverbird theme list and
weaverbird theme show [name] inspect the catalog. Full detail, including the exact appearance and bar
style resolution order and the color value formats, is SPEC.md section 6.2.
To see a theme applied without waiting on a real Claude Code session, use weaverbird preview
(below), which renders through the exact same theme and layout engine as the real bar.
Layout
By default weaverbird renders every enabled provider's widgets, in provider order, except any a
provider marked "default": false: an opt-in, group-only widget that exists but does not clutter the
bar unasked (handy for a chatty or niche widget most people never want to see). For coarse control over
what goes on the bar and in what order, including pulling an opt-in widget in, a provider can declare
named groups (curated bundles of its own widgets, e.g. bloodhound.quota), and you can point
~/.claude/weaverbird/config.json's layout at an ordered list of tokens:
{ "layout": ["bloodhound", "usage-gov.pace", "pigeon"] }
Each token is, in precedence order, a group id, a widget id, or a provider name (that provider's own
implicit default group, every widget it declares, opt-in ones excluded). When layout is set, it is
strict: only the widgets it resolves to render, in that order; anything not referenced is left off the
bar, opt-in or not. An opt-in widget is still selectable, overriding its own default, either by naming
its widget id directly or through a group that includes it. When layout is absent, the default view
above applies. Manage it with weaverbird layout show (prints the resolved layout, by row, plus opt-in
widgets available to add, what a strict layout omits, and any unknown token), weaverbird layout set <token...>, and weaverbird layout clear. Full detail, including the exact token precedence, is
SPEC.md section 6.3 (and section 3.4 for opt-in widgets).
Commands
weaverbird read the session JSON on stdin and print the bar (the statusLine command)
weaverbird install claim the statusLine slot (backs up settings first)
weaverbird doctor verify the slot is owned; list providers, widgets, and health
weaverbird list list discovered providers and their widgets
weaverbird enable <name> re-enable a provider
weaverbird disable <name> disable a provider without deleting it
weaverbird preview render the bar in a normal terminal, outside a Claude Code session
weaverbird theme list list built-in and user themes, marking the active one
weaverbird theme show [name] print a theme's light and dark palettes
weaverbird theme use <name> set the active theme
weaverbird theme appearance <auto|light|dark> set the appearance pointer
weaverbird theme bar <glyph|track> override how a meter's bar is drawn
weaverbird layout show print the resolved layout: what renders, what's omitted, unknown tokens
weaverbird layout set <token...> set the active layout
weaverbird layout clear remove the layout, returning to the default (every widget)
weaverbird version print the version
weaverbird preview runs the real render pipeline outside a session: by default it synthesizes a
session payload from your environment (cwd, $CLAUDE_CODE_SESSION_ID or a stable placeholder,
your real terminal width) and drives your actual providers, so you see your actual current bar.
--input FILE (or - for stdin) replays a captured statusline payload instead. --demo bypasses
the real providers and renders a fixed, clearly-synthetic widget set that exercises every class, the
icon dedup, and the width cascade, so a theme can be fully evaluated even when real provider quota is
low. --width, --theme, --appearance, and --layout (comma-separated tokens) override those
settings for that one render only, without touching config.json.
Prior art
weaverbird's contract deliberately recombines established conventions rather than inventing a format:
the class field for semantic state from Waybar, full_text/short_text width degradation and
min_width from the i3bar protocol, the declaration-versus-render split and the caching and truncation
rules from linesmith, the structured drop-in record from snackdriven/claude-statusline, manifest
discovery and graceful degradation from betmoar/cc-status-plugin, HTML's <meter> (a measurement within
a known range) for the kind of that name, and the typed content-block shape (a type discriminator
beside a block named for it) from the Claude API for how a widget's kind decides its own nested field
block. SPEC.md credits each in detail, and is explicit about where it departs: Waybar's
threshold-to-class states map is deliberately not adopted, because a provider computes its own
severity and sends it as class.
Scope
This is built to solve one real problem well, the author's three tools sharing one bar, and to be
useful with a single provider and nobody else's. It is not framed as a standard. If the contract is
clean enough that others find it worth implementing, that is earned later, not declared here.