
Statusline Generator
- 901 installs
- 1.3k repo stars
- Updated August 4, 2026
- daymade/claude-code-skills
statusline-generator is a Claude Code skill that displays live session costs and daily usage totals in the terminal prompt by integrating the ccusage CLI for developers who need budget visibility without leaving their co
About
statusline-generator is a Claude Code statusline skill that surfaces real-time API spend directly in the terminal prompt by calling the ccusage command-line tool. The statusline script invokes `ccusage session --json --offline` and `ccusage daily --json --offline`, parsing totals with jq to show session and daily cost figures formatted to two decimal places. Developers reach for statusline-generator when running long Claude Code sessions and need immediate cost feedback—avoiding post-hoc transcript audits or surprise daily bills. The skill includes a ccusage integration reference for troubleshooting offline JSON parsing and prompt formatting.
- Integrates with ccusage to display current session and daily Claude costs
- Uses JSON output, offline mode, and descending sort for fast retrieval
- Implements 2-minute caching with background refresh to prevent prompt lag
- Graceful error suppression and fallback to previous cache values
- Shows costs only after first background fetch completes
Statusline Generator by the numbers
- 901 all-time installs (skills.sh)
- +39 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #538 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/daymade/claude-code-skills --skill statusline-generatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 901 |
|---|---|
| repo stars | ★ 1.3k |
| Security audit | 3 / 3 scanners passed |
| Last updated | August 4, 2026 |
| Repository | daymade/claude-code-skills ↗ |
How do you show Claude Code costs in the terminal prompt?
See live Claude Code session costs and daily totals directly in their terminal prompt without breaking workflow.
Who is it for?
Claude Code users who want live session and daily cost totals in their terminal prompt during long agent coding sessions.
Skip if: Developers using Cursor or Codex without Claude Code statusline support, or teams needing org-wide billing dashboards.
When should I use this skill?
A developer asks to add cost tracking to Claude Code statusline, integrate ccusage, or show session spend in the prompt.
What you get
Custom Claude Code statusline script displaying session and daily cost totals from ccusage
- statusline shell script
- live cost display in prompt
By the numbers
- Cost values formatted to two decimal places via printf
- Uses ccusage session and daily JSON offline modes
Files
Statusline Generator
A single-source-of-truth statusline for Claude Code. One script, two layouts, end-to-end self-verification.
Quick health check (start here when something is wrong)
Run this first whenever the statusline misbehaves. It catches the silent failures that account for most "configured but not working" reports:
bash scripts/health_check.shIt validates four layers: 1. ~/.claude/statusline.sh exists and is executable. Missing `chmod +x` is the single most common silent-failure cause — Claude Code runs the script, exec fails, statusline goes blank. 2. ~/.claude/settings.json has a valid statusLine block pointing at the script. 3. Mock stdin tests covering complete data, zero tokens, missing fields, and $HOME path shortening. 4. Real stdin replay from /tmp/.claude-statusline-last-stdin.json if you previously ran with CLAUDE_STATUSLINE_DEBUG=1.
Each failure prints a one-line fix command — you don't have to read documentation to recover.
Quick install
bash scripts/install_statusline.shThis script:
- Backs up any existing
~/.claude/statusline.shandsettings.json. - Copies
generate_statusline.shto~/.claude/statusline.shandchmod +xs it. - Updates
settings.jsonstatusLineblock viajq(preserves other settings). - Mandatorily runs `health_check.sh` and shows the result — installation
is not "complete" until verification passes.
Restart Claude Code (or send any new message) to see the statusline update.
What you get
Default — minimal one-line layout
~/code/myproject Opus 4.7 (1M context) ctx: 108K / 1MJust the essentials: short path, model name, absolute token counts. No colors, no git, no cost, no percentage. Designed for users who want signal without noise.
Full — multi-line with cost and git
Set CLAUDE_STATUSLINE_LAYOUT=full in your shell profile to enable:
alex (Sonnet 4.6) [$0.42/$25.93] ctx: 108K/1M (11%)
~/code/myproject
[git:main*+]- Line 1: user, model, ccusage session/daily costs, color-coded ctx (green ≤50%,
yellow 51–80%, red >80%).
- Line 2: short path.
- Line 3: git branch with
*for modified,+for untracked.
Layouts: how to switch
The script reads layout from environment, not flags (Claude Code passes JSON on stdin, so flags would conflict). Set in ~/.zshrc or ~/.bashrc:
# Minimal (default — same as not setting it)
export CLAUDE_STATUSLINE_LAYOUT=minimal
# Full
export CLAUDE_STATUSLINE_LAYOUT=fullRestart your shell (or source the rc file) so Claude Code inherits the change, then send a message — statusline refreshes within 300ms.
Debug stdin capture
To see exactly what JSON Claude Code sends your script:
export CLAUDE_STATUSLINE_DEBUG=1Each invocation writes its stdin to /tmp/.claude-statusline-last-stdin.json (overwriting on every refresh). Inspect with jq .. Useful for:
- Diagnosing why a field doesn't render the way you expect.
- Re-running the script against real input:
cat /tmp/.claude-statusline-last-stdin.json | ~/.claude/statusline.sh. - Filing bug reports — paste the dump as ground truth.
Authoring rules (why this skill is shaped this way)
Two production failure modes drove the current design. Both are sealed in code, not just docs:
Rule 1 — Always chmod +x, always verify by running
The single biggest silent-failure cause of any statusline is a script without the executable bit: Claude Code's exec fails silently and the bar goes blank with no error. install_statusline.sh always chmod +xs; health_check.sh flags the bit if missing. If you hand-write or hand-edit a statusline script, mock-test it before declaring done: echo '{}' | bash your-script.sh.
Rule 2 — "Configuration complete" is meaningless without evidence
"Wrote the file and updated settings.json" is not the same as "the script runs and produces the expected output." install_statusline.sh therefore always runs health_check.sh at the end and exits non-zero if any check fails. Treat any "complete!" report from any agent that lacks evidence as suspect.
For field-level traps (used_percentage null at session start, total_input_tokens semantics across Claude Code versions, hardcoded context_window_size), see `references/context-window-schema.md`.
Customization
For colors, custom segments (hostname, time, etc.), and disabling cost tracking, see `references/customization.md`.
Dependencies
The script auto-detects available tools and degrades gracefully:
| Tool | Required for | Fallback |
|---|---|---|
jq | JSON parsing (preferred) | falls back to python3 |
python3 | JSON parsing fallback | bare cwd only |
awk | token K/M formatting | required by both layouts |
git | git status (full layout) | silent skip if missing or not in repo |
ccusage | cost (full layout) | silent skip if missing |
Install on macOS: brew install jq. On Debian/Ubuntu: apt install jq.
Troubleshooting
For symptom-by-symptom diagnostics, see `references/troubleshooting-decision-tree.md`. It walks through:
1. Statusline blank or never updates (chmod cause) 2. ctx segment missing or wrong (field traps) 3. Want token counts not percentages (layout switch) 4. Colors render as raw escape codes (terminal compatibility) 5. Git segment missing (full layout) 6. Cost segment missing (ccusage / cache) 7. Edits have no effect (path mismatch) 8. Slow refresh (jq vs python3)
Resources
| File | Purpose |
|---|---|
scripts/generate_statusline.sh | The statusline script. Single source of truth. Two layouts via CLAUDE_STATUSLINE_LAYOUT. |
scripts/install_statusline.sh | Idempotent installer. Backs up, copies, chmods, wires settings.json, runs health check. |
scripts/health_check.sh | Four-layer verification: file perms, settings.json wiring, mock stdin tests, real stdin replay. |
references/troubleshooting-decision-tree.md | Symptom-driven diagnostic flowchart. Load when statusline misbehaves. |
references/customization.md | Color changes, custom segments, threshold tuning, single-line full layout. Load when user wants to modify how the statusline looks. |
references/context-window-schema.md | Claude Code statusline JSON schema. Documents every field plus current_usage vs total_input_tokens semantics across versions. |
references/color_codes.md | ANSI color codes reference. Load for color customization. |
references/ccusage_integration.md | ccusage integration deep-dive: caching, JSON shape, troubleshooting. Load for cost-related issues. |
Security scan passed
Scanned at: 2026-06-13T19:44:41.599869
Tool: gitleaks + pattern-based validation
Content hash: 7de155173bc4809f8b8f687299d451e4ea91e5c9324b15bab5a49a0a5ca115cb
ccusage Integration Reference
This reference explains how the statusline integrates with ccusage for cost tracking and troubleshooting.
What is ccusage?
ccusage is a command-line tool that tracks Claude Code usage and costs by reading conversation transcripts. It provides session-based and daily cost reporting.
How Statusline Uses ccusage
The statusline script calls ccusage to display session and daily costs:
session=$(ccusage session --json --offline -o desc 2>/dev/null | jq -r '.sessions[0].totalCost' 2>/dev/null | xargs printf "%.2f")
daily=$(ccusage daily --json --offline -o desc 2>/dev/null | jq -r '.daily[0].totalCost' 2>/dev/null | xargs printf "%.2f")Key Features
1. JSON Output: Uses --json flag for machine-readable output 2. Offline Mode: Uses --offline to avoid fetching pricing data (faster) 3. Descending Order: Uses -o desc to get most recent data first 4. Error Suppression: Redirects errors to /dev/null to prevent statusline clutter
Caching Strategy
To avoid slowing down the statusline, costs are cached:
- Cache File:
/tmp/claude_cost_cache_YYYYMMDD_HHMM.txt - Cache Duration: 2 minutes (refreshes based on minute timestamp)
- Background Refresh: First run fetches costs in background
- Fallback: Uses previous cache (up to 10 minutes old) while refreshing
Cache Behavior
1. First Display: Statusline shows without costs 2. 2-5 Seconds Later: Costs appear after background fetch completes 3. Next 2 Minutes: Cached costs shown instantly 4. After 2 Minutes: New cache generated in background
ccusage JSON Structure
Session Data
{
"sessions": [
{
"sessionId": "conversation-id",
"totalCost": 0.26206769999999996,
"inputTokens": 2065,
"outputTokens": 1313,
"lastActivity": "2025-10-20"
}
]
}Daily Data
{
"daily": [
{
"date": "2025-10-20",
"totalCost": 25.751092800000013,
"inputTokens": 16796,
"outputTokens": 142657
}
]
}Troubleshooting
Costs Not Showing
Symptoms: Statusline appears but no [$X.XX/$X.XX] shown
Possible Causes: 1. ccusage not installed 2. ccusage not in PATH 3. No transcript data available yet 4. Background fetch still in progress
Solutions:
# Check if ccusage is installed
which ccusage
# Test ccusage manually
ccusage session --json --offline -o desc
# Check cache files
ls -lh /tmp/claude_cost_cache_*.txt
# Wait 5-10 seconds and check again (first fetch runs in background)Slow Statusline
Symptoms: Statusline takes >1 second to appear
Possible Causes: 1. Cache not working (being regenerated too often) 2. ccusage taking too long to execute
Solutions:
# Check cache timestamp
ls -lh /tmp/claude_cost_cache_*.txt
# Test ccusage speed
time ccusage session --json --offline -o desc
# If slow, consider disabling cost tracking by commenting out cost section in scriptIncorrect Costs
Symptoms: Costs don't match expected values
Possible Causes: 1. Cache stale (showing old data) 2. ccusage database out of sync 3. Multiple Claude sessions confusing costs
Solutions:
# Clear cache to force refresh
rm /tmp/claude_cost_cache_*.txt
# Verify ccusage data
ccusage session -o desc | head -20
ccusage daily -o desc | head -20
# Check ccusage database location
ls -lh ~/.config/ccusage/Installing ccusage
If ccusage is not installed:
# Using npm (Node.js required)
npm install -g @anthropic-ai/ccusage
# Or check the official ccusage repository for latest installation instructionsDisabling Cost Tracking
To disable costs (e.g., if ccusage not available), comment out the cost section in generate_statusline.sh:
# Cost information using ccusage with caching
cost_info=""
# cache_file="/tmp/claude_cost_cache_$(date +%Y%m%d_%H%M).txt"
# ... rest of cost section commented outThen update the final printf to remove %s for cost_info:
printf '\033[01;32m%s\033[00m \033[01;36m(%s)\033[00m\n\033[01;37m%s\033[00m\n%s' \
"$username" "$model" "$short_path" "$git_info"ANSI Color Codes Reference
This reference provides ANSI escape codes for customizing statusline colors.
Format
ANSI color codes follow this format:
\033[<attributes>m<text>\033[00m\033[- Escape sequence start<attributes>- Color and style codes (see below)m- Marks end of escape sequence\033[00m- Reset to default
Common Color Codes
Regular Colors
\033[00;30m- Black\033[00;31m- Red\033[00;32m- Green\033[00;33m- Yellow\033[00;34m- Blue\033[00;35m- Magenta\033[00;36m- Cyan\033[00;37m- White
Bright/Bold Colors (Used in Default Statusline)
\033[01;30m- Bright Black (Gray)\033[01;31m- Bright Red\033[01;32m- Bright Green\033[01;33m- Bright Yellow\033[01;34m- Bright Blue\033[01;35m- Bright Magenta\033[01;36m- Bright Cyan\033[01;37m- Bright White
Default Statusline Colors
The generated statusline uses these colors by default:
| Element | Color Code | Color Name | Visibility |
|---|---|---|---|
| Username | \033[01;32m | Bright Green | Excellent |
| Model | \033[01;36m | Bright Cyan | Excellent |
| Costs | \033[01;35m | Bright Magenta | Excellent |
| Path | \033[01;37m | Bright White | Excellent |
| Git (clean) | \033[01;33m | Bright Yellow | Excellent |
| Git (dirty) | \033[01;31m | Bright Red | Excellent |
Customizing Colors
To customize colors in the statusline script, edit the printf statements:
Example: Change username to bright blue
# Original:
printf '\033[01;32m%s\033[00m' "$username"
# Modified:
printf '\033[01;34m%s\033[00m' "$username"Example: Change path to yellow
# Original:
printf '\033[01;37m%s\033[00m' "$short_path"
# Modified:
printf '\033[01;33m%s\033[00m' "$short_path"Testing Colors
Test color codes in terminal:
echo -e "\033[01;32mGreen\033[00m \033[01;36mCyan\033[00m \033[01;35mMagenta\033[00m"Tips
1. Always reset: End each colored section with \033[00m to reset colors 2. Visibility: Bright colors (01;3X) are more visible than regular (00;3X) 3. Contrast: Choose colors that contrast well with your terminal background 4. Consistency: Use consistent colors for similar elements across your environment
Statusline Input JSON Schema
The statusline script receives a JSON object on stdin. This reference documents all known fields.
Top-Level Fields
{
"session_id": "uuid",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/current/working/dir",
"version": "2.1.121",
"fast_mode": false,
"exceeds_200k_tokens": false,
"output_style": { "name": "default" },
"model": {
"id": "model-id-string",
"display_name": "Human-Readable Model Name"
},
"workspace": {
"current_dir": "/path/to/cwd",
"project_dir": "/path/to/project",
"added_dirs": ["/additional/dir"]
},
"cost": {
"total_cost_usd": 1.0868,
"total_duration_ms": 342863,
"total_api_duration_ms": 282122,
"total_lines_added": 0,
"total_lines_removed": 0
},
"context_window": { ... },
"effort": { "level": "max" },
"thinking": { "enabled": true }
}context_window Object (Key Fields)
{
"context_window": {
"context_window_size": 1000000,
"used_percentage": 9,
"remaining_percentage": 91,
"total_input_tokens": 83320,
"total_output_tokens": 5456,
"current_usage": {
"input_tokens": 29,
"output_tokens": 163,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 88832
}
}
}Field Meanings
| Field | Type | Description |
|---|---|---|
context_window_size | number | Model's total context window in tokens (e.g., 1000000 = 1M) |
used_percentage | number | Current context usage as percentage (0-100, may have decimals) |
remaining_percentage | number | Remaining context capacity as percentage |
total_input_tokens | number | Tokens currently in the context window (sum of input_tokens + cache_creation_input_tokens + cache_read_input_tokens). Before Claude Code v2.1.132 this was session-cumulative and could exceed `context_window_size`. |
total_output_tokens | number | Output tokens from the most recent response. Before v2.1.132 this was session-cumulative. |
current_usage.input_tokens | number | Current turn uncached input tokens |
current_usage.output_tokens | number | Current turn output tokens |
current_usage.cache_creation_input_tokens | number | Tokens written to prompt cache this turn |
current_usage.cache_read_input_tokens | number | Tokens read from prompt cache this turn |
Computing Actual Context Usage
Correct formula:
current_context_used = current_usage.input_tokens
+ current_usage.cache_read_input_tokens
+ current_usage.cache_creation_input_tokensThis sum should approximately equal context_window_size * used_percentage / 100.
*Prefer `current_usage. summed.** It is correct on every Claude Code version (0 at session start, never null, never cumulative). total_input_tokens is equivalent on v2.1.132 and later but was session-cumulative before that and could exceed context_window_size. If you need to support older Claude Code versions, use current_usage`.
Model Context Window Sizes (Reference)
| Model Family | Typical context_window_size |
|---|---|
| Claude Opus 4.x | 200,000 |
| Claude Sonnet 4.x | 200,000 |
| Claude Opus 4.5+ | 500,000 |
| Claude Sonnet 4.5+ | 1,000,000 |
| DeepSeek V4 Flash | 200,000 |
| DeepSeek V4 Pro | 1,000,000 |
| GPT-5.x | varies |
Always read context_window_size from the JSON — never hardcode it.
cost Object
| Field | Type | Description |
|---|---|---|
total_cost_usd | number | Session total cost in USD (may have >2 decimals) |
total_duration_ms | number | Total wall-clock duration in ms |
total_api_duration_ms | number | Total API call duration in ms |
total_lines_added | number | Lines added this session |
total_lines_removed | number | Lines removed this session |
Customization
How to modify the statusline beyond layout switching. Load this file when the user wants colors, custom segments, or to disable specific features.
Change colors (full layout)
Colors are ANSI escape codes in printf calls inside render_full(). See `color_codes.md` for the complete code reference.
Example — recolor the username from green to blue:
# In scripts/generate_statusline.sh, find this line in render_full:
printf '\033[01;32m%s\033[00m \033[01;36m(%s)\033[00m%s%s\n...' "$username" ...
# ^^^^^^^^ green (32)
# Change to blue (34):
printf '\033[01;34m%s\033[00m \033[01;36m(%s)\033[00m%s%s\n...' "$username" ...Standard color codes:
| Code | Color |
|---|---|
\033[01;30m | Bright black (gray) |
\033[01;31m | Bright red |
\033[01;32m | Bright green |
\033[01;33m | Bright yellow |
\033[01;34m | Bright blue |
\033[01;35m | Bright magenta |
\033[01;36m | Bright cyan |
\033[01;37m | Bright white |
\033[00m | Reset |
After editing, always verify with bash scripts/health_check.sh to confirm mock tests still pass.
Add custom segments (full layout)
Extend render_full() to add hostname, time, weather, or anything else.
Pattern:
render_full() {
# ... existing code ...
# Your new segment
local hostname segment_color
hostname=$(hostname -s)
segment_color="\033[01;34m" # blue
# Add to the existing printf format string
printf '\033[01;32m%s@%s\033[00m \033[01;36m(%s)\033[00m%s%s\n...' \
"$username" "$hostname" "$model" "$cost_info" "$ctx_display"
# ^^^^^ ^^^^^^^^^ added
}Pattern — time:
local now
now=$(date +%H:%M)
# Add %s and "$now" to the printfKeep additions side-effect-free so health_check.sh mock tests remain deterministic. After editing, run bash scripts/health_check.sh.
Disable cost tracking (full layout)
Cost requires ccusage. If ccusage is not installed, cost_info is empty automatically — no edit needed, the segment silently disappears.
To skip the lookup entirely (saves ~50ms on each refresh):
In scripts/generate_statusline.sh, locate the cost block in render_full():
if command -v ccusage >/dev/null 2>&1; then
# ... cache + ccusage logic ...
fiWrap or comment out the entire if block. Health check will still pass.
Switch to single-line full layout
The default full layout is multi-line (3 lines: header / path / git). To collapse it into a single line, edit the final printf in render_full():
# Three-line (default full):
printf '\033[01;32m%s\033[00m \033[01;36m(%s)\033[00m%s%s\n\033[01;37m%s\033[00m\n%s' \
"$username" "$model" "$cost_info" "$ctx_display" "$short_path" "$git_info"
# Single-line full:
printf '\033[01;36m[%s]\033[00m \033[01;37m%s\033[00m %s%s | \033[01;32m$%s\033[00m' \
"$model" "$short_path" "$git_info" "$ctx_display" "$cost"Change ctx color thresholds
Default thresholds (full layout): green ≤50%, yellow 51–80%, red >80%.
To change, edit in render_full():
local ctx_color="\033[01;32m" # green ≤50%
if [ "${ctx_pct_int:-0}" -gt 80 ]; then
ctx_color="\033[01;31m" # red >80%
elif [ "${ctx_pct_int:-0}" -gt 50 ]; then
ctx_color="\033[01;33m" # yellow 51-80%
fiAdjust the 80 and 50 cutoffs to taste.
Where to put the env var setting
For CLAUDE_STATUSLINE_LAYOUT and CLAUDE_STATUSLINE_DEBUG to apply to Claude Code, the variables must be exported in the shell before Claude Code starts. Set them in:
- macOS / Linux:
~/.zshrc,~/.bashrc,~/.profile, or~/.config/fish/config.fish - Per-project:
.envrc(with direnv)
Restart the shell or source the rc file, then start Claude Code.
Troubleshooting Decision Tree
When the statusline misbehaves, run the health check first — it covers most issues:
bash scripts/health_check.shIf the health check passes but you still see an issue, walk the symptoms below.
---
Symptom 1: Statusline is blank or never updates
Most common root cause: the script is not executable. Claude Code runs the configured command and silently shows nothing if the exec fails. This is the single biggest source of "I configured it but nothing happens" reports.
Diagnose:
ls -la "$(jq -r '.statusLine.command' ~/.claude/settings.json | sed "s|^~|$HOME|; s|^bash ||")"Look at the leftmost column. If it does not start with -rwx, the script is missing its executable bit.
Fix:
chmod +x ~/.claude/statusline.sh(Substitute the actual path from your settings.json.)
Other possible causes:
settings.jsonstatusLine.commandpoints to a path that no longer exists.
Run bash scripts/health_check.sh to confirm.
- The script's shebang references an interpreter that's not installed. Verify
with head -1 ~/.claude/statusline.sh — should be #!/usr/bin/env bash or similar.
- The script writes to stderr instead of stdout. Claude Code only displays
stdout. Pipe a mock stdin and inspect: echo '{}' | ~/.claude/statusline.sh.
---
Symptom 2: ctx: ... segment is missing or shows wrong numbers
Root cause A: `used_percentage` is `null` early in a session.
The Claude Code docs explicitly warn that context_window.used_percentage and remaining_percentage "may be null early in the session." If your script gates the ctx segment on these fields, the segment vanishes until the first real API response populates them. A naive // empty filter swallows the output entirely.
Fix: This skill's generate_statusline.sh computes used tokens from current_usage.input_tokens + cache_read_input_tokens + cache_creation_input_tokens, which is 0 (not null) at session start, so the ctx segment renders even before any real usage.
Root cause B: confusing `total_input_tokens` with current context.
In Claude Code v2.1.131 and earlier, total_input_tokens was a session-cumulative count and could exceed context_window_size. Using it as "current usage" shows percentages above 100% in long sessions.
Fix: Use current_usage.* summed (this skill's default). It always reflects the current context state regardless of Claude Code version.
Root cause C: `context_window_size` missing from JSON.
Some early versions or non-standard clients omit this field. The script then divides by zero (or skips the segment).
Diagnose:
export CLAUDE_STATUSLINE_DEBUG=1
# Send any message in Claude Code, then:
jq '.context_window' /tmp/.claude-statusline-last-stdin.jsonIf context_window_size is missing, your Claude Code is too old or running a non-standard runtime. Update Claude Code.
---
Symptom 3: I want token counts, not percentages
The default layout shows ctx: 108K / 1M (token counts only). If yours shows percentages, you are running the full layout.
Switch to minimal:
Remove this from your shell rc file:
export CLAUDE_STATUSLINE_LAYOUT=fullOr set it explicitly to minimal:
export CLAUDE_STATUSLINE_LAYOUT=minimalThen start a new shell so Claude Code inherits the new env.
---
Symptom 4: Colors look wrong or render as literal escape codes
Diagnose:
echo '{"workspace":{"current_dir":"/tmp"},"model":{"display_name":"M"},"context_window":{"context_window_size":100000,"used_percentage":80,"current_usage":{"input_tokens":80000,"cache_read_input_tokens":0,"cache_creation_input_tokens":0}}}' | \
CLAUDE_STATUSLINE_LAYOUT=full ~/.claude/statusline.sh | cat -vIf you see ^[[01;33m instead of yellow text, your terminal is not interpreting ANSI escape codes.
Fix:
- Most modern terminals (iTerm2, Apple Terminal, Kitty, Windows Terminal,
WezTerm) support these by default. If yours doesn't, switch terminal.
- Claude Code itself renders the statusline output and supports ANSI colors,
so this issue is rare during actual use — only shows up when you pipe the script manually.
---
Symptom 5: Git status segment is missing (full layout)
Root cause A: You're not inside a git repository. Expected — git segment is silent outside repos.
Root cause B: git binary not installed.
Diagnose:
command -v git || echo "git not installed"
git -C "$PWD" rev-parse --git-dir 2>&1Fix: Install git, or accept that the segment is hidden.
Root cause C: Permission errors reading .git/.
ls -la "$(git rev-parse --git-dir 2>/dev/null)" 2>&1 | head -3---
Symptom 6: Cost segment is missing (full layout)
The cost segment depends on ccusage being installed and on PATH. It runs asynchronously with a 2-minute cache, so the first display after install is expected to lack it; the cache populates within 5–10 seconds.
Diagnose:
command -v ccusage || echo "ccusage not installed"
ccusage session --json --offline -o desc 2>&1 | head -20
ls -lh /tmp/claude_cost_cache_*.txt 2>/dev/nullFix:
- Install
ccusage:npm install -g ccusage(or see theccusagerepo for
current install instructions).
- Wait for the background fetch on first use.
- Force refresh:
rm /tmp/claude_cost_cache_*.txtthen trigger a statusline
update by sending any message in Claude Code.
For deeper details and offline data structure, see `ccusage_integration.md`.
---
Symptom 7: I edited the script but my changes have no effect
Claude Code reads the script every refresh, so live edits should take effect on the next statusline update (typically the next assistant message).
Diagnose:
# Confirm Claude Code actually runs your edited script:
export CLAUDE_STATUSLINE_DEBUG=1
# Then send a message in Claude Code and check the dump:
ls -la /tmp/.claude-statusline-last-stdin.jsonIf the dump's mtime updates after your message, the script is being executed. If your edits still have no effect, try bash health_check.sh to confirm the script being run is actually the one you edited (it might be a stale copy at a different path).
Possible mismatch: settings.json statusLine.command points to one path, but you edited a different file. The health check warns when these diverge.
---
Symptom 8: Script is slow (statusline appears with delay)
Diagnose:
time (echo '{"workspace":{"current_dir":"/tmp"},"model":{"display_name":"M"},"context_window":{"context_window_size":1000,"current_usage":{"input_tokens":0,"cache_read_input_tokens":0,"cache_creation_input_tokens":0}}}' | ~/.claude/statusline.sh)Should be well under 100ms. If much slower, suspect:
- `ccusage` blocking: Should be backgrounded by the script. Verify with
time ccusage session --json --offline -o desc | head -1.
- `git` slow on a large repo: Switch to minimal layout to skip git lookup.
- `python3` fallback hot path: Install
jq(brew install jq).
---
When all else fails
Capture both real stdin and the script's actual output, then re-run health check:
export CLAUDE_STATUSLINE_DEBUG=1
# Send a message in Claude Code, wait 1 second, then:
bash scripts/health_check.sh
echo "---"
echo "Real stdin Claude Code sent:"
jq . /tmp/.claude-statusline-last-stdin.json
echo "---"
echo "Script output for that stdin:"
cat /tmp/.claude-statusline-last-stdin.json | ~/.claude/statusline.shThe combination of health_check.sh results plus the captured stdin/output pair is enough evidence to diagnose virtually any issue.
#!/usr/bin/env bash
# Claude Code statusline — single source of truth.
#
# Default (minimal): ~/short/path Model Name ctx: 108K / 1M
# Full layout:
# user (Model) [$session/$daily] ctx: 108K / 1M (11%)
# ~/short/path
# [git:branch*+]
#
# Configuration via environment variables (no flags — Claude Code passes JSON on stdin):
# CLAUDE_STATUSLINE_LAYOUT=full enable multi-line cost/git/percentage layout
# CLAUDE_STATUSLINE_DEBUG=1 dump stdin to /tmp/.claude-statusline-last-stdin.json
# (for end-to-end verification, see health_check.sh)
#
# Dependencies: jq preferred (python3 fallback). awk for number formatting.
# Optional: git (for full layout's git status), ccusage (for cost display in full layout).
input=$(cat)
if [ -n "$CLAUDE_STATUSLINE_DEBUG" ]; then
printf '%s' "$input" > /tmp/.claude-statusline-last-stdin.json 2>/dev/null
fi
LAYOUT="${CLAUDE_STATUSLINE_LAYOUT:-minimal}"
# ---------- JSON field extraction (jq preferred, python3 fallback) ----------
parse_with_jq() {
echo "$input" | jq -r '
[
(.model.display_name // "Claude"),
(.workspace.current_dir // ""),
(.context_window.context_window_size // 0),
(.context_window.current_usage.input_tokens // 0),
(.context_window.current_usage.cache_read_input_tokens // 0),
(.context_window.current_usage.cache_creation_input_tokens // 0),
(.context_window.used_percentage // 0),
(.cost.total_cost_usd // 0)
] | @tsv
'
}
parse_with_python() {
echo "$input" | python3 -c '
import json, sys
d = json.load(sys.stdin)
cw = d.get("context_window") or {}
cu = cw.get("current_usage") or {}
print("\t".join(str(v) for v in [
d.get("model", {}).get("display_name", "Claude"),
d.get("workspace", {}).get("current_dir", ""),
cw.get("context_window_size", 0) or 0,
cu.get("input_tokens", 0) or 0,
cu.get("cache_read_input_tokens", 0) or 0,
cu.get("cache_creation_input_tokens", 0) or 0,
cw.get("used_percentage", 0) or 0,
(d.get("cost") or {}).get("total_cost_usd", 0) or 0,
]))
'
}
if command -v jq >/dev/null 2>&1; then
parsed=$(parse_with_jq)
elif command -v python3 >/dev/null 2>&1; then
parsed=$(parse_with_python)
else
# No JSON parser — degrade to bare cwd
echo "$PWD"
exit 0
fi
IFS=$'\t' read -r model_full cwd ctx_size ctx_input ctx_cache_read ctx_cache_create ctx_pct cost_raw <<< "$parsed"
cwd="${cwd:-$PWD}"
ctx_size="${ctx_size:-0}"
ctx_used=$((${ctx_input:-0} + ${ctx_cache_read:-0} + ${ctx_cache_create:-0}))
ctx_pct_int=$(printf '%.0f' "${ctx_pct:-0}" 2>/dev/null || echo 0)
short_path="${cwd/#$HOME/~}"
# ---------- Helpers ----------
# Format token counts: 999 / 108K / 1M / 1.5M
human_tokens() {
awk -v n="${1:-0}" 'BEGIN {
n = n + 0
if (n >= 1000000) {
m = n / 1000000
if (m == int(m)) printf "%dM", m
else printf "%.1fM", m
} else if (n >= 1000) {
printf "%dK", int(n/1000 + 0.5)
} else {
printf "%d", n
}
}'
}
# ---------- Minimal layout (default) ----------
render_minimal() {
local out="$short_path"
[ -n "$model_full" ] && out="${out} ${model_full}"
if [ "${ctx_size:-0}" -gt 0 ] 2>/dev/null; then
out="${out} ctx: $(human_tokens "$ctx_used") / $(human_tokens "$ctx_size")"
fi
echo "$out"
}
# ---------- Full layout (multi-line: cost + git + percentage) ----------
render_full() {
local username
username=$(whoami)
# Color threshold by ctx percentage
local ctx_color="\033[01;32m" # green ≤50%
if [ "${ctx_pct_int:-0}" -gt 80 ]; then
ctx_color="\033[01;31m" # red >80%
elif [ "${ctx_pct_int:-0}" -gt 50 ]; then
ctx_color="\033[01;33m" # yellow 51-80%
fi
# Model name shortening: "Sonnet 4.5 (with 1M token context)" -> "Sonnet 4.5 [1M]"
local model
model=$(echo "$model_full" \
| sed -E 's/\(with ([0-9]+[KM]) token context\)/[\1]/' \
| sed -E 's/\[1m\]/[1M]/' \
| sed 's/ *$//')
# Git status (best-effort; silent if not a git repo or git missing)
local git_info=""
if command -v git >/dev/null 2>&1 && \
git -C "$cwd" --no-optional-locks rev-parse --git-dir >/dev/null 2>&1; then
local branch status=""
branch=$(git -C "$cwd" --no-optional-locks branch --show-current 2>/dev/null || echo "detached")
if ! git -C "$cwd" --no-optional-locks diff --quiet 2>/dev/null || \
! git -C "$cwd" --no-optional-locks diff --cached --quiet 2>/dev/null; then
status="*"
fi
if [ -n "$(git -C "$cwd" --no-optional-locks ls-files --others --exclude-standard 2>/dev/null)" ]; then
status="${status}+"
fi
if [ -n "$status" ]; then
git_info=$(printf '\033[01;31m[git:%s%s]\033[00m' "$branch" "$status")
else
git_info=$(printf '\033[01;33m[git:%s]\033[00m' "$branch")
fi
fi
# Cost via ccusage (cached, async; silent if ccusage unavailable)
local cost_info=""
if command -v ccusage >/dev/null 2>&1; then
local cache_file="/tmp/claude_cost_cache_$(date +%Y%m%d_%H%M).txt"
find /tmp -maxdepth 1 -name "claude_cost_cache_*.txt" -mmin +2 -delete 2>/dev/null
if [ -f "$cache_file" ]; then
cost_info=$(cat "$cache_file")
else
{
local session daily
if command -v jq >/dev/null 2>&1; then
session=$(ccusage session --json --offline -o desc 2>/dev/null | jq -r '.sessions[0].totalCost // 0' | xargs printf "%.2f" 2>/dev/null)
daily=$(ccusage daily --json --offline -o desc 2>/dev/null | jq -r '.daily[0].totalCost // 0' | xargs printf "%.2f" 2>/dev/null)
else
session=$(ccusage session --json --offline -o desc 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); s=d.get('sessions',[{}])[0]; print(f\"{s.get('totalCost',0):.2f}\")" 2>/dev/null)
daily=$(ccusage daily --json --offline -o desc 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); s=d.get('daily',[{}])[0]; print(f\"{s.get('totalCost',0):.2f}\")" 2>/dev/null)
fi
if [ -n "$session" ] && [ -n "$daily" ]; then
printf ' \033[01;35m[$%s/$%s]\033[00m' "$session" "$daily" > "$cache_file"
fi
} &
local prev_cache
prev_cache=$(find /tmp -maxdepth 1 -name "claude_cost_cache_*.txt" -mmin -10 2>/dev/null | head -1)
[ -f "$prev_cache" ] && cost_info=$(cat "$prev_cache")
fi
fi
# Context display
local ctx_display=""
if [ "${ctx_size:-0}" -gt 0 ] 2>/dev/null; then
ctx_display=$(printf " ${ctx_color}ctx: %s/%s (%s%%)\033[00m" \
"$(human_tokens "$ctx_used")" "$(human_tokens "$ctx_size")" "$ctx_pct_int")
fi
# Three lines: header / path / git
printf '\033[01;32m%s\033[00m \033[01;36m(%s)\033[00m%s%s\n\033[01;37m%s\033[00m\n%s' \
"$username" "$model" "$cost_info" "$ctx_display" "$short_path" "$git_info"
}
case "$LAYOUT" in
full) render_full ;;
minimal|*) render_minimal ;;
esac
#!/usr/bin/env bash
# Statusline health check — verify configuration end-to-end.
#
# Runs four layers of verification:
# 1. Script exists and is executable (chmod +x).
# 2. settings.json points to the script and uses type=command.
# 3. Mock stdin tests (minimal + edge cases) — script must output expected shape.
# 4. Real stdin replay (if /tmp/.claude-statusline-last-stdin.json exists from CLAUDE_STATUSLINE_DEBUG).
#
# Usage:
# bash health_check.sh # checks ~/.claude/statusline.sh
# bash health_check.sh /custom/path.sh # checks custom script
set -u
SCRIPT_PATH="${1:-$HOME/.claude/statusline.sh}"
SETTINGS_FILE="$HOME/.claude/settings.json"
DEBUG_DUMP="/tmp/.claude-statusline-last-stdin.json"
# Color output only if stdout is a terminal
if [ -t 1 ]; then
G='\033[01;32m'; R='\033[01;31m'; Y='\033[01;33m'; B='\033[01;36m'; N='\033[00m'
else
G=''; R=''; Y=''; B=''; N=''
fi
PASS=0
FAIL=0
WARN=0
ok() { printf "${G}✓${N} %s\n" "$1"; PASS=$((PASS+1)); }
fail() { printf "${R}✗${N} %s\n" "$1"; [ -n "${2:-}" ] && printf " ${B}fix:${N} %s\n" "$2"; FAIL=$((FAIL+1)); }
warn() { printf "${Y}⚠${N} %s\n" "$1"; [ -n "${2:-}" ] && printf " ${B}note:${N} %s\n" "$2"; WARN=$((WARN+1)); }
section() { printf "\n${B}== %s ==${N}\n" "$1"; }
# ---------- 1. Script existence + permissions ----------
section "1. Script file"
if [ ! -f "$SCRIPT_PATH" ]; then
fail "Script not found: $SCRIPT_PATH" "copy generate_statusline.sh from this skill to that path"
echo
printf "${R}HARD FAIL — cannot continue checks${N}\n"
exit 1
fi
ok "Script exists: $SCRIPT_PATH"
if [ ! -x "$SCRIPT_PATH" ]; then
fail "Script not executable (this is the #1 silent-failure cause)" "chmod +x '$SCRIPT_PATH'"
else
ok "Script is executable (chmod +x)"
fi
# ---------- 2. settings.json wiring ----------
section "2. settings.json wiring"
if [ ! -f "$SETTINGS_FILE" ]; then
fail "settings.json not found: $SETTINGS_FILE" "create it with statusLine block, see SKILL.md Quick Start"
elif command -v jq >/dev/null 2>&1; then
sl_type=$(jq -r '.statusLine.type // "MISSING"' "$SETTINGS_FILE" 2>/dev/null)
sl_cmd=$(jq -r '.statusLine.command // "MISSING"' "$SETTINGS_FILE" 2>/dev/null)
if [ "$sl_type" = "MISSING" ]; then
fail "settings.json has no statusLine block" "run install_statusline.sh"
elif [ "$sl_type" != "command" ]; then
fail "statusLine.type is '$sl_type', expected 'command'" "set statusLine.type to \"command\""
else
ok "statusLine.type = command"
fi
if [ "$sl_cmd" = "MISSING" ]; then
fail "statusLine.command is missing" "set it to a path or shell command"
else
# Expand ~ for comparison
sl_cmd_expanded="${sl_cmd/#\~/$HOME}"
# Strip leading "bash " if user wrapped it
sl_cmd_stripped="${sl_cmd_expanded#bash }"
if [ "$sl_cmd_stripped" = "$SCRIPT_PATH" ] || [ "$sl_cmd_expanded" = "$SCRIPT_PATH" ]; then
ok "statusLine.command points to $SCRIPT_PATH"
else
warn "statusLine.command = '$sl_cmd' (expected to reference $SCRIPT_PATH)" \
"edit settings.json statusLine.command if you intended to use this script"
fi
fi
else
warn "jq not installed — cannot validate settings.json structure" "brew install jq (or apt install jq)"
fi
# ---------- 3. Mock stdin tests ----------
section "3. Mock stdin tests (minimal layout)"
run_mock() {
local label="$1" json="$2" expect_pattern="$3"
local out
out=$(echo "$json" | "$SCRIPT_PATH" 2>&1)
if echo "$out" | grep -Eq "$expect_pattern"; then
ok "$label"
printf " output: %s\n" "$out"
else
fail "$label — output didn't match expected shape" "expected pattern: $expect_pattern; got: $out"
fi
}
# Test 1: complete data
run_mock "complete data → renders cwd + model + ctx" \
'{"workspace":{"current_dir":"/tmp/test"},"model":{"display_name":"TestModel"},"context_window":{"context_window_size":1000000,"current_usage":{"input_tokens":50000,"cache_read_input_tokens":0,"cache_creation_input_tokens":50000}}}' \
'TestModel.*ctx: 100K / 1M'
# Test 2: zero tokens (session start)
run_mock "0 tokens (session just started)" \
'{"workspace":{"current_dir":"/tmp/test"},"model":{"display_name":"M"},"context_window":{"context_window_size":1000000,"current_usage":{"input_tokens":0,"cache_read_input_tokens":0,"cache_creation_input_tokens":0}}}' \
'ctx: 0 / 1M'
# Test 3: missing context_window entirely
run_mock "missing context_window field → no ctx segment" \
'{"workspace":{"current_dir":"/tmp/test"},"model":{"display_name":"M"}}' \
'/tmp/test M'
# Test 4: cwd in HOME → tilde shortening
run_mock "cwd in HOME → ~ short path" \
"{\"workspace\":{\"current_dir\":\"$HOME/x/y\"},\"model\":{\"display_name\":\"M\"}}" \
'~/x/y'
# ---------- 4. Real stdin replay (if available) ----------
section "4. Real stdin replay"
if [ -f "$DEBUG_DUMP" ]; then
out=$(cat "$DEBUG_DUMP" | "$SCRIPT_PATH" 2>&1)
if [ -n "$out" ]; then
ok "Real stdin from $DEBUG_DUMP renders successfully"
printf " output: %s\n" "$out"
else
fail "Real stdin produced empty output" "inspect $DEBUG_DUMP and run: cat $DEBUG_DUMP | $SCRIPT_PATH"
fi
else
warn "No real stdin dump available at $DEBUG_DUMP" \
"to capture: export CLAUDE_STATUSLINE_DEBUG=1, send any message in Claude Code, re-run this check"
fi
# ---------- Summary ----------
section "Summary"
printf "Pass: ${G}%d${N} Fail: ${R}%d${N} Warn: ${Y}%d${N}\n" "$PASS" "$FAIL" "$WARN"
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
exit 0
#!/usr/bin/env bash
# Install statusline script + wire settings.json + run health check.
#
# After install, the script always finishes with a health_check.sh run so the
# user sees concrete pass/fail evidence (not just "Installation complete").
# This prevents the silent-failure mode where chmod is forgotten or the
# settings.json command points to the wrong path.
#
# Usage:
# bash install_statusline.sh # install to ~/.claude/statusline.sh
# bash install_statusline.sh /custom/path.sh # install to custom path
set -e
TARGET_PATH="${1:-$HOME/.claude/statusline.sh}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SOURCE_SCRIPT="$SCRIPT_DIR/generate_statusline.sh"
HEALTH_CHECK="$SCRIPT_DIR/health_check.sh"
SETTINGS_FILE="$HOME/.claude/settings.json"
TARGET_DIR=$(dirname "$TARGET_PATH")
if [ ! -f "$SOURCE_SCRIPT" ]; then
echo "ERROR: generate_statusline.sh not found at $SOURCE_SCRIPT" >&2
exit 1
fi
if [ ! -d "$TARGET_DIR" ]; then
echo ">> Creating directory: $TARGET_DIR"
mkdir -p "$TARGET_DIR"
fi
# Backup existing target script if present
if [ -f "$TARGET_PATH" ]; then
backup="$TARGET_PATH.bak.$(date +%Y%m%d_%H%M%S)"
echo ">> Backing up existing script: $backup"
cp "$TARGET_PATH" "$backup"
fi
echo ">> Installing: $SOURCE_SCRIPT -> $TARGET_PATH"
cp "$SOURCE_SCRIPT" "$TARGET_PATH"
chmod +x "$TARGET_PATH" # critical — silent failure root cause if missed
# Wire settings.json
if [ ! -f "$SETTINGS_FILE" ]; then
echo ">> Creating new settings.json with statusLine block"
cat > "$SETTINGS_FILE" <<EOF
{
"statusLine": {
"type": "command",
"command": "$TARGET_PATH",
"padding": 0
}
}
EOF
elif command -v jq >/dev/null 2>&1; then
settings_backup="$SETTINGS_FILE.bak.$(date +%Y%m%d_%H%M%S)"
cp "$SETTINGS_FILE" "$settings_backup"
echo ">> Backed up settings.json: $settings_backup"
tmp=$(mktemp)
jq --arg cmd "$TARGET_PATH" \
'.statusLine = {"type":"command","command":$cmd,"padding":(.statusLine.padding // 0)}' \
"$SETTINGS_FILE" > "$tmp"
mv "$tmp" "$SETTINGS_FILE"
echo ">> Updated settings.json statusLine.command -> $TARGET_PATH"
else
echo "WARN: jq not installed — cannot safely edit settings.json" >&2
echo " Manually set statusLine.command to: $TARGET_PATH" >&2
fi
# ---- Mandatory health check (do not let the user discover failures themselves) ----
echo
echo "== Running health check =="
if [ -x "$HEALTH_CHECK" ] || bash "$HEALTH_CHECK" --help >/dev/null 2>&1; then
bash "$HEALTH_CHECK" "$TARGET_PATH" || {
echo
echo "WARN: Health check reported issues. Review the output above." >&2
echo " Re-run anytime: bash $HEALTH_CHECK $TARGET_PATH" >&2
exit 1
}
else
echo "WARN: health_check.sh not found or not executable — skipping post-install verification" >&2
fi
echo
echo "Installation complete."
echo
echo "Usage:"
echo " Default : minimal one-line layout (cwd + model + ctx tokens)"
echo " Full layout : add to ~/.zshrc or ~/.bashrc:"
echo " export CLAUDE_STATUSLINE_LAYOUT=full"
echo " (multi-line with cost via ccusage + git status + percentage)"
echo " Debug stdin : export CLAUDE_STATUSLINE_DEBUG=1"
echo " dumps each invocation's stdin to /tmp/.claude-statusline-last-stdin.json"
echo
echo "Verify anytime: bash $HEALTH_CHECK"
Related skills
How it compares
Choose statusline-generator over manual ccusage checks when you need persistent in-prompt cost visibility during every session.
FAQ
What tool does statusline-generator use for costs?
statusline-generator integrates ccusage, a CLI that reads Claude Code conversation transcripts and reports session-based and daily cost totals as JSON for statusline display.
Which costs appear in the statusline?
statusline-generator shows the current session totalCost and the most recent daily totalCost, both formatted to two decimal places in the Claude Code terminal prompt.
Is Statusline Generator safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.