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 uses
  • context_window — token counts, used percentage, cache stats
  • workspace — project directory
  • version — Claude Code version
  • output_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:

  1. Drop priority: 3 sections, right-to-left
  2. Still too wide? Drop priority: 2, right-to-left
  3. priority: 1 sections 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 color
  • custom[].color.match — map output values to colors
  • custom[].default_color — default color for custom commands (use instead of color to avoid conflict with color.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

  1. Each cached value has a key (e.g., vcs_branch) and a TTL in seconds
  2. On first call, the command runs and its stdout is written to $CACHE_DIR/$key
  3. On subsequent calls within the TTL, the cached file is read instead
  4. 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.sh should 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:

  1. global.width in config — explicit override
  2. CLAUDE_STATUSLINE_WIDTH env var
  3. Process tree walk — finds an ancestor process with a real TTY and runs stty size on it
  4. stty size < /dev/tty — works on some systems
  5. COLUMNS env var
  6. tput cols
  7. 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

Description
No description provided
Readme MIT 248 KiB
Languages
Rust 96.9%
Shell 3.1%