claude-statusline
A configurable, multi-line status line for Claude Code with priority-based flex layout, VCS auto-detection (git/jj), spacer-based positioning, custom command sections, and per-section formatting.
[Opus] | Anthropic | gitlore master* +1-0
[============----] 58% | $0.12 | +156 -23 | 14m | 7 tools (Edit)
Quick Start
Prerequisites
- bash 4+ (macOS ships bash 3 —
brew install bash) - jq (
brew install jq/apt install jq) - Claude Code with status line support
Install
git clone https://github.com/tayloreernisse/claude-statusline ~/projects/claude-statusline
cd ~/projects/claude-statusline
./install.sh
The installer creates two symlinks:
~/.claude/statusline.sh -> ~/projects/claude-statusline/statusline.sh
~/.claude/statusline.json -> ~/projects/claude-statusline/statusline.json
Configure Claude Code
Add to ~/.claude/settings.json:
{
"statusLine": "~/.claude/statusline.sh"
}
Restart Claude Code to see the status line.
How It Works
Claude Code (every ~300ms)
|
| pipes JSON to stdin
v
statusline.sh
|
+-- reads ~/.claude/statusline.json (config)
+-- parses stdin JSON (model, cost, context, etc.)
+-- caches expensive ops (VCS, beads, system load)
+-- resolves layout (preset or custom array)
+-- renders sections with ANSI colors
+-- applies priority-based flex layout
|
| stdout (ANSI-colored lines)
v
Claude Code renders status bar
The stdin JSON from Claude Code contains:
model— current model info (id, display name)cost— accumulated cost, duration, lines changed, tool usescontext_window— token counts, used percentage, cache statsworkspace— project directoryversion— Claude Code versionoutput_style— current output style
Configuration Reference
Config lives at ~/.claude/statusline.json (or set CLAUDE_STATUSLINE_CONFIG env var).
Global Options
{
"global": {
"separator": " | ",
"justify": "left",
"vcs": "auto",
"width_margin": 4,
"cache_dir": "/tmp/claude-sl-{session_id}"
}
}
| Option | Default | Description |
|---|---|---|
separator |
" | " |
Text between sections. In left mode, used as-is. In spread/space-between, the non-space characters (e.g. |) stay as visual anchors with extra padding around them. |
justify |
"left" |
How sections distribute across terminal width. See Justify Modes. |
width |
(auto) | Explicit terminal width in columns. If omitted, auto-detection walks the process tree to find an ancestor with a real TTY, falling back to stty via /dev/tty, COLUMNS, tput cols, or 120. |
width_margin |
4 |
Columns to subtract from the detected width. Accounts for terminal multiplexer borders (Zellij/tmux) or Claude Code UI chrome that reduce the actual visible area. |
vcs |
"auto" |
VCS detection: auto, git, jj, none |
cache_dir |
/tmp/claude-sl-{session_id} |
Cache directory template. {session_id} is replaced at runtime. |
Layout System
The layout field controls which sections appear and on which lines.
Using a preset:
{ "layout": "standard" }
Using a custom layout:
{
"layout": [
["model", "provider", "project", "spacer", "vcs"],
["context_bar", "cost", "duration"]
]
}
Each inner array is one line. Section IDs in the array are rendered left-to-right, separated by the global separator.
Spacer Sections
Use spacer (or _spacer1, _spacer2, etc. for multiple spacers) as a virtual section that expands to fill remaining width. This pushes sections apart without using justify: "spread".
{
"layout": [
["model", "provider", "spacer", "vcs"]
]
}
Output (80 columns):
[Opus] | Anthropic master* +1-0
Spacers have flex: true by default and suppress the separator on adjacent sides. Use multiple unique spacer IDs (_spacer1, _spacer2) if you need more than one on a line.
Built-in Presets
standard (2 lines):
Line 1: model | provider | project | spacer | vcs
Line 2: context_bar | cost | lines_changed | duration | tools
dense (1 line):
Line 1: model | provider | project | vcs | context_bar | cost | duration
verbose (3 lines):
Line 1: model | provider | project | vcs | beads
Line 2: context_bar | tokens_raw | cache_efficiency | cost | cost_velocity
Line 3: lines_changed | duration | tools | turns | load | version
You can define custom presets in the presets object and reference them by name.
Section Configuration
Each section supports these common properties:
{
"enabled": true,
"priority": 1,
"flex": false,
"min_width": 0,
"prefix": "",
"suffix": "",
"pad": 0,
"align": "left",
"color": "dim"
}
| Property | Type | Description |
|---|---|---|
enabled |
boolean | Whether the section renders at all |
priority |
1/2/3 | Display priority (see Flex Layout below) |
flex |
boolean | Whether section expands to fill remaining width |
min_width |
integer | Minimum character width |
prefix |
string | Text prepended to section output |
suffix |
string | Text appended to section output |
pad |
integer | Pad output to this minimum width |
align |
left/right/center |
Alignment within padded width |
color |
color name | Override the section's color (see Color Names) |
Per-Section Formatting Examples
Add brackets around the project name:
{
"sections": {
"project": { "prefix": "[", "suffix": "]" }
}
}
Output: [myproject]
Right-align cost in a fixed-width column:
{
"sections": {
"cost": { "pad": 8, "align": "right" }
}
}
Output: $0.12 (padded to 8 chars)
Override a section's default color:
{
"sections": {
"duration": { "color": "cyan" }
}
}
Flex Layout Deep Dive
Priority Drop
When a line is wider than the terminal:
- Drop
priority: 3sections, right-to-left - Still too wide? Drop
priority: 2, right-to-left priority: 1sections never drop
Example at 120 columns (everything fits):
[Opus] | Anthropic | gitlore | master* | [======----] 58% | $0.12 | +156 -23 | 14m | 7 tools
Same content at 80 columns (priority 3 dropped, priority 2 trimmed):
[Opus] | gitlore | master* | [======----] 58% | $0.12
Flex Expansion
If a section has flex: true and there's remaining width after rendering all sections, the flex section expands to fill the gap. For context_bar, this means a wider progress bar. For spacers, they expand to push adjacent sections apart. For other sections, it pads with spaces.
One flex section per line wins. Spacers take priority over non-spacer flex sections.
80 columns with flex context_bar:
[Opus] | gitlore | master* | [================----] 58% | $0.12
80 columns with spacer (pushes VCS to right):
[Opus] | gitlore master* +1-0
Justify Modes
The global.justify setting controls how sections distribute across the full terminal width. This is independent of priority drop — sections are dropped first if needed, then the remaining sections are distributed.
"left" (default) — Pack sections left with fixed-width separators. Use flex: true on a section to fill remaining space.
[Opus] | Anthropic | gitlore | master*
"spread" — Distribute extra space evenly into all gaps. The separator's visible characters (e.g. |) stay as anchors, padded with spaces on both sides. Every line fills the full terminal width.
[Opus] | Anthropic | gitlore | master*
"space-between" — Same distribution as spread for terminal output (first section starts at column 0, last section ends at the terminal edge).
When justify is "spread" or "space-between", the flex property on individual sections is ignored — the entire line spreads instead of one section expanding.
The separator character still matters: " | " produces gaps like | , while " :: " produces :: , and " " (pure spaces) produces invisible gaps.
Section Reference
model
Source: stdin JSON model.display_name or parsed from model.id
Shows the current model name in bold brackets. Falls back to parsing "Opus"/"Sonnet"/"Haiku" from the model ID.
[Opus]
provider
Source: pattern match on model.id
Detects the API provider from the model identifier.
| Pattern | Provider |
|---|---|
us.anthropic.*, anthropic.* |
Bedrock |
*@YYYYMMDD |
Vertex |
claude-* |
Anthropic |
Empty if provider can't be determined.
project
Source: basename of workspace.project_dir
Shows the current project directory name.
vcs
Source: git/jj CLI (cached)
Auto-detects VCS by checking for .jj/ first, then .git/. Override with vcs.prefer.
{
"vcs": {
"prefer": "auto",
"show_ahead_behind": true,
"show_dirty": true,
"ttl": { "branch": 3, "dirty": 5, "ahead_behind": 30 }
}
}
git output: master* +1-0 (branch, dirty indicator, ahead/behind)
jj output: feature-branch* (bookmark or short change ID, dirty indicator)
Each piece has its own cache TTL — branch name barely changes, dirty status changes often, ahead/behind is expensive.
beads
Source: br CLI (cached)
Shows current in-progress bead and ready count. Requires br to be installed.
{
"beads": {
"show_wip": true,
"show_ready_count": true,
"ttl": 30
}
}
br-47 wip | 3 ready
context_bar
Source: context_window.used_percentage
Visual progress bar of context window usage with color thresholds.
{
"context_bar": {
"flex": true,
"bar_width": 10,
"thresholds": { "warn": 50, "danger": 70, "critical": 85 }
}
}
| Range | Color |
|---|---|
| 0-49% | Green |
| 50-69% | Yellow |
| 70-84% | Red |
| 85%+ | Bold Red |
context_usage
Source: context_window.total_input_tokens, total_output_tokens, max_tokens, used_percentage
Shows context usage as "used/total" (e.g., 125k/200k). The "used" part is colored based on thresholds, while "total" is always dim.
{
"context_usage": {
"enabled": true,
"thresholds": { "warn": 50, "danger": 70, "critical": 85 }
}
}
Output: 125k/200k (green when below 50%, yellow 50-69%, red 70-84%, bold red 85%+)
If max_tokens is not provided in the input, it's calculated from the percentage.
tokens_raw
Source: context_window.total_input_tokens, total_output_tokens
Raw token counts in human-readable format.
{ "tokens_raw": { "format": "{input}in/{output}out" } }
115kin/8.5kout
cache_efficiency
Source: cache_read_input_tokens, cache_creation_input_tokens
Percentage of cache reads vs total cache operations.
cache:33%
cost
Source: cost.total_cost_usd
Accumulated session cost with color thresholds.
{
"cost": {
"thresholds": { "warn": 0.25, "danger": 0.50, "critical": 1.00 }
}
}
cost_velocity
Source: cost / duration
Cost per minute of session time.
$0.08/m
token_velocity
Source: tokens / duration
Total tokens (input + output) consumed per minute. Useful for understanding how fast you're burning through context.
14.5ktok/m
In narrow terminals, abbreviated to 14.5kt/m.
lines_changed
Source: cost.total_lines_added, total_lines_removed
+156 -23
Green for additions, red for removals.
duration
Source: cost.total_duration_ms
Human-readable session duration: 14m, 1h23m, 45s.
tools
Source: cost.total_tool_uses, cost.last_tool_name
{
"tools": {
"show_last_name": true,
"ttl": 2
}
}
7 tools (Edit)
turns
Source: cost.total_turns
12 turns
load
Source: system load average (macOS: sysctl, Linux: /proc/loadavg)
{ "load": { "ttl": 10 } }
load:2.1
version
Source: version from stdin JSON
v1.0.80
time
Source: date command
{ "time": { "format": "%H:%M" } }
output_style
Source: output_style.name
Shows the current Claude Code output style (e.g., "learning", "concise").
hostname
Source: hostname -s
Short hostname of the machine.
Custom Commands
Add custom sections that execute shell commands and display the result.
{
"custom": [
{
"id": "ollama",
"label": "ollama",
"command": "pgrep -x ollama >/dev/null && echo 'up' || echo 'down'",
"ttl": 30,
"priority": 3,
"color": {
"match": { "up": "green", "down": "dim" }
}
}
]
}
Then reference by id in any layout line:
{ "layout": [["model", "project", "ollama"]] }
Custom Command Fields
| Field | Required | Default | Description |
|---|---|---|---|
id |
yes | — | Unique identifier, used in layout arrays |
label |
no | same as id |
Display prefix before the value |
command |
yes | — | Shell command. stdout is captured. |
ttl |
no | 30 | Cache TTL in seconds |
priority |
no | 2 | Display priority (1/2/3) |
flex |
no | false | Expand to fill remaining width |
min_width |
no | 4 | Minimum character width |
color.match |
no | — | Map output values to color names |
default_color |
no | — | Fallback color when color.match doesn't match |
prefix |
no | — | Text prepended to output |
suffix |
no | — | Text appended to output |
pad |
no | — | Pad output to this minimum width |
align |
no | left |
Alignment within padded width (left/right/center) |
Color Names
Available colors: red, green, yellow, blue, magenta, cyan, white, dim, bold.
These work in three places:
sections.<name>.color— override a built-in section's colorcustom[].color.match— map output values to colorscustom[].default_color— default color for custom commands (use instead ofcolorto avoid conflict withcolor.match)
Recipe Ideas
Dev server status:
{
"id": "devserver",
"command": "curl -so /dev/null http://localhost:3000 && echo 'up' || echo 'down'",
"ttl": 10,
"priority": 3,
"color": { "match": { "up": "green", "down": "red" } }
}
Disk usage:
{
"id": "disk",
"label": "disk",
"command": "df -h / | awk 'NR==2{print $5}'",
"ttl": 120,
"priority": 3
}
CI status (via gh):
{
"id": "ci",
"label": "CI",
"command": "gh run list --limit 1 --json conclusion -q '.[0].conclusion' 2>/dev/null || echo '?'",
"ttl": 60,
"priority": 3,
"color": { "match": { "success": "green", "failure": "red", "?": "dim" } }
}
Ollama status:
{
"id": "ollama",
"label": "ollama",
"command": "pgrep -x ollama >/dev/null && echo 'up' || echo 'down'",
"ttl": 30,
"priority": 3,
"color": { "match": { "up": "green", "down": "dim" } }
}
Docker status:
{
"id": "docker",
"label": "docker",
"command": "docker info >/dev/null 2>&1 && echo 'up' || echo 'down'",
"ttl": 60,
"priority": 3,
"color": { "match": { "up": "green", "down": "dim" } }
}
Caching
Expensive operations (VCS commands, beads queries, custom commands, system load) are cached in files under the cache directory.
How It Works
- Each cached value has a key (e.g.,
vcs_branch) and a TTL in seconds - On first call, the command runs and its stdout is written to
$CACHE_DIR/$key - On subsequent calls within the TTL, the cached file is read instead
- After TTL expires, the command runs again
Cache Location
Default: /tmp/claude-sl-{session_id}/
The {session_id} template variable is replaced with the script's PID. Configure via global.cache_dir.
TTLs by Section
| Section | Key | Default TTL |
|---|---|---|
| vcs (branch) | vcs_branch |
3s |
| vcs (dirty) | vcs_dirty |
5s |
| vcs (ahead/behind) | vcs_ab |
30s |
| beads (wip) | beads_wip |
30s |
| beads (ready) | beads_ready |
30s |
| load | load |
10s |
| custom commands | custom_{id} |
per-command ttl |
Troubleshooting
Script doesn't run
- Check the shebang:
head -1 ~/.claude/statusline.shshould be#!/usr/bin/env bash - Verify it's executable:
ls -la ~/.claude/statusline.sh(should show-rwxr-xr-x) - Fix:
chmod +x ~/.claude/statusline.sh - Verify jq is installed:
which jq - Test manually:
echo '{}' | ~/.claude/statusline.sh
No output / blank status line
- Test with mock data:
echo '{"model":{"id":"claude-opus-4-5-20251101","display_name":"Opus"},"cost":{"total_cost_usd":0.05}}' | ~/.claude/statusline.sh - Check config is valid JSON:
jq . ~/.claude/statusline.json - Check symlinks:
ls -la ~/.claude/statusline.sh ~/.claude/statusline.json
Stale data
- VCS branch stuck? TTL is 3s by default. Increase:
sections.vcs.ttl.branch - Clear cache:
rm -rf /tmp/claude-sl-*/
Performance
The script runs every ~300ms. Most sections are free (parsed from stdin JSON). Expensive sections:
| Section | Cost | Mitigation |
|---|---|---|
| vcs | git/jj subprocess | Cached with per-field TTLs |
| beads | br subprocess | 30s TTL |
| load | sysctl/proc read | 10s TTL |
| custom | arbitrary command | User-configured TTL |
If status line feels sluggish, increase TTLs or disable expensive sections.
Provider Detection
The provider section detects the API provider from the model ID:
| Model ID Pattern | Detected Provider |
|---|---|
us.anthropic.claude-* |
Bedrock |
anthropic.claude-* |
Bedrock |
claude-*-*@20251101 |
Vertex |
claude-opus-4-5-20251101 |
Anthropic (direct) |
Detection is pure pattern matching on model.id — no credentials or API calls involved.
Config Validation
A JSON Schema is provided at schema.json for editor autocomplete and validation.
VSCode: Add to your statusline.json:
{
"$schema": "./schema.json",
"version": 1
}
CLI validation (with ajv):
npx ajv validate -s schema.json -d statusline.json
Terminal Width Detection
Claude Code runs the status line script without a TTY, which makes detecting the terminal width non-trivial. The detection priority is:
global.widthin config — explicit overrideCLAUDE_STATUSLINE_WIDTHenv var- Process tree walk — finds an ancestor process with a real TTY and runs
stty sizeon it stty size < /dev/tty— works on some systemsCOLUMNSenv vartput cols- Fallback: 120
After detection, global.width_margin (default: 4) is subtracted to account for terminal multiplexer borders or UI chrome.
Environment Variables
| Variable | Description |
|---|---|
CLAUDE_STATUSLINE_CONFIG |
Override config file path |
CLAUDE_STATUSLINE_WIDTH |
Override terminal width (second priority after global.width) |
COLUMNS |
Standard terminal width (lower priority) |
CLI Flags
The script supports several flags for development and debugging:
# Test your config with mock data (no Claude Code needed)
./statusline.sh --test
# Get path to JSON schema for IDE autocomplete
./statusline.sh --config-schema
# Debug internal state (width detection, theme, VCS, etc.)
./statusline.sh --dump-state
# Show help
./statusline.sh --help
--test Mode
Renders the status line with realistic mock data. Useful for:
- Validating config changes without restarting Claude Code
- Rapid iteration on layout and colors
- Debugging section visibility
--dump-state Mode
Outputs internal computed state as JSON:
{
"terminal": {"detected_width": 178, "effective_width": 174, "width_tier": "wide"},
"responsive": {"enabled": true, "layout": "verbose"},
"theme": "dark",
"vcs": "git",
"paths": {"config": "~/.claude/statusline.json", "cache_dir": "/tmp/claude-sl-abc123"}
}
Examples
The examples/ directory contains ready-to-use configurations:
| File | Description |
|---|---|
dense.json |
Single-line layout with compact context bar |
verbose.json |
Three-line layout with all metrics |
custom-commands.json |
Custom Ollama and Docker status indicators |
Copy an example to your config:
cp examples/verbose.json ~/.claude/statusline.json
License
MIT