# 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 ```bash 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`: ```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 ```json { "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](#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:** ```json { "layout": "standard" } ``` **Using a custom layout:** ```json { "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"`. ```json { "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: ```json { "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](#color-names)) ### Per-Section Formatting Examples **Add brackets around the project name:** ```json { "sections": { "project": { "prefix": "[", "suffix": "]" } } } ``` Output: `[myproject]` **Right-align cost in a fixed-width column:** ```json { "sections": { "cost": { "pad": 8, "align": "right" } } } ``` Output: ` $0.12` (padded to 8 chars) **Override a section's default color:** ```json { "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`. ```json { "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. ```json { "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. ```json { "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. ```json { "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. ```json { "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. ```json { "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` ```json { "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`) ```json { "load": { "ttl": 10 } } ``` ``` load:2.1 ``` ### version **Source:** `version` from stdin JSON ``` v1.0.80 ``` ### time **Source:** `date` command ```json { "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. ```json { "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: ```json { "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..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:** ```json { "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:** ```json { "id": "disk", "label": "disk", "command": "df -h / | awk 'NR==2{print $5}'", "ttl": 120, "priority": 3 } ``` **CI status (via gh):** ```json { "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:** ```json { "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:** ```json { "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`: ```json { "$schema": "./schema.json", "version": 1 } ``` **CLI validation (with ajv):** ```bash 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: ```bash # 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: ```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: ```bash cp examples/verbose.json ~/.claude/statusline.json ``` ## License MIT