ccppccpp home

Tools / Configuration

Claude Code status line editor

beta

Claude Code runs a command of your choice to draw the status line under the prompt and pipes it the session's JSON: model, context window, cost, rate limits, git worktree, pull request. Arrange those fields on up to six lines, color them, add thresholds and links, and watch the preview against sample sessions. The editor writes a bash script that reads the JSON with jq and the statusLine entry of settings.json, or one command that installs both.

Preview

Your status line under the Claude Code prompt, drawn from a sample session.

> fix the failing checkout test
[Opus] ๐Ÿ“ shop | ๐ŸŒฟ feat/status-line +2 ~3
โ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ | 42% | $1.87 | โฑ 45m 45s
  • The preview uses sample git values; the script asks git for the real ones.

Rate limits, an open pull request, a warm cache.

Columns, the COLUMNS Claude Code gives the script.

Edit the sample JSON

The JSON Claude Code pipes to the script. Remove a field to see what happens when it is absent.

Lines

Line 1

Line 2

Select a widget in a line to edit it.

Settings

Extra columns before the status line.

Empty: run only on events.

Install

Paste it in a terminal (bash or zsh). It writes ~/.claude/statusline.sh, sets statusLine in ~/.claude/settings.json and keeps a .bak copy of both. Needs jq.

bash
mkdir -p ~/.claude
[ -f ~/.claude/statusline.sh ] && cp ~/.claude/statusline.sh ~/.claude/statusline.sh.bak
cat > ~/.claude/statusline.sh <<'CCPP_STATUSLINE'
#!/usr/bin/env bash
# Claude Code status line from https://ccpp.dev/tools/claude-code-statusline-editor
# Claude Code pipes the session JSON to stdin (https://code.claude.com/docs/en/statusline). Needs jq.
# ccpp-layout: {"version":1,"lines":[{"separator":" ","separatorStyle":{"fg":"bright-black","bg":"default","bold":false,"dim":false,"italic":false,"underline":false},"segments":[{"id":"model","widget":"model","variant":"","text":"","prefix":"[","suffix":"]","width":10,"thresholds":{"enabled":false,"warn":70,"critical":90},"link":false,"style":{"fg":"cyan","bg":"default","bold":false,"dim":false,"italic":false,"underline":false}},{"id":"dir","widget":"directory","variant":"","text":"","prefix":"๐Ÿ“ ","suffix":"","width":10,"thresholds":{"enabled":false,"warn":70,"critical":90},"link":false,"style":{"fg":"default","bg":"default","bold":false,"dim":false,"italic":false,"underline":false}},{"id":"branch","widget":"git-branch","variant":"","text":"","prefix":"| ๐ŸŒฟ ","suffix":"","width":10,"thresholds":{"enabled":false,"warn":70,"critical":90},"link":false,"style":{"fg":"default","bg":"default","bold":false,"dim":false,"italic":false,"underline":false}},{"id":"changes","widget":"git-changes","variant":"","text":"","prefix":"","suffix":"","width":10,"thresholds":{"enabled":false,"warn":70,"critical":90},"link":false,"style":{"fg":"yellow","bg":"default","bold":false,"dim":false,"italic":false,"underline":false}}]},{"separator":" | ","separatorStyle":{"fg":"bright-black","bg":"default","bold":false,"dim":false,"italic":false,"underline":false},"segments":[{"id":"bar","widget":"bar","variant":"","text":"โ–ˆโ–‘","prefix":"","suffix":"","width":10,"thresholds":{"enabled":true,"warn":70,"critical":90},"link":false,"style":{"fg":"default","bg":"default","bold":false,"dim":false,"italic":false,"underline":false}},{"id":"ctx","widget":"context-percent","variant":"","text":"","prefix":"","suffix":"","width":10,"thresholds":{"enabled":true,"warn":70,"critical":90},"link":false,"style":{"fg":"default","bg":"default","bold":false,"dim":false,"italic":false,"underline":false}},{"id":"cost","widget":"cost","variant":"","text":"","prefix":"","suffix":"","width":10,"thresholds":{"enabled":false,"warn":70,"critical":90},"link":false,"style":{"fg":"yellow","bg":"default","bold":false,"dim":false,"italic":false,"underline":false}},{"id":"time","widget":"duration","variant":"","text":"","prefix":"โฑ ","suffix":"","width":10,"thresholds":{"enabled":false,"warn":70,"critical":90},"link":false,"style":{"fg":"default","bg":"default","bold":false,"dim":false,"italic":false,"underline":false}}]}],"padding":0,"refreshInterval":null,"hideVimModeIndicator":false}

input=$(cat)
command -v jq >/dev/null 2>&1 || { echo "statusline: jq is not installed (https://jqlang.org)"; exit 0; }

dir=$(printf '%s' "$input" | jq -r '.workspace.current_dir // .cwd // empty')
branch="" staged=0 modified=0
if [ -n "$dir" ] && git -C "$dir" rev-parse --git-dir >/dev/null 2>&1; then
  branch=$(git -C "$dir" branch --show-current 2>/dev/null)
  [ -n "$branch" ] || branch=$(git -C "$dir" rev-parse --short HEAD 2>/dev/null)
  staged=$(git --no-optional-locks -C "$dir" diff --cached --numstat 2>/dev/null | wc -l | tr -d ' ')
  modified=$(git --no-optional-locks -C "$dir" diff --numstat 2>/dev/null | wc -l | tr -d ' ')
fi

program=$(cat <<'CCPP_JQ'
def num: if type == "number" then . else null end;
def str: if type == "string" and . != "" then . else null end;
def rep($n): if $n <= 0 then "" else . * $n end;
def pct: if . == null then null else "\(floor)%" end;
def usd: if . == null then null else (. * 100 | round) as $c | "$\($c / 100 | floor)." + ("0\($c % 100)" | .[-2:]) end;
def tok: if . == null then null elif . >= 1000000 then "\(. / 100000 | floor | . / 10)M" elif . >= 1000 then "\(. / 100 | floor | . / 10)k" else "\(floor)" end;
def dur: if . == null then null else floor as $s | if $s >= 86400 then "\($s / 86400 | floor)d \($s % 86400 / 3600 | floor)h" elif $s >= 3600 then "\($s / 3600 | floor)h \($s % 3600 / 60 | floor)m" elif $s >= 60 then "\($s / 60 | floor)m \($s % 60)s" else "\($s)s" end end;
def left: if . == null then null else (. - now) as $d | if $d <= 0 then null else $d | dur end end;
def bar($w; $full; $empty): if . == null then null else ([0, ([$w, (. * $w / 100 | floor)] | min)] | max) as $f | ($full | rep($f)) + ($empty | rep($w - $f)) end;
def base: if . == null then null else ([splits("[/\\\\]")] | map(select(. != "")) | last) // "/" end;
def tilde: if . == null then null else ($ENV.HOME // "") as $h | if $h != "" and (. == $h or startswith($h + "/")) then "~" + .[($h | length):] else . end end;
def scalar: if type == "string" then str elif type == "number" or type == "boolean" then tostring elif . == null then null else tojson end;
def paint($open): if $open == "" then . else $open + . + "\u001b[0m" end;
def link($url): if $url == null or $url == "" then . else "\u001b]8;;" + $url + "\u0007" + . + "\u001b]8;;\u0007" end;

([
  (try ((.model.display_name | str) as $t | "\u001b[36m" as $open | if $t == null or $t == "" then null else ("[" + ($t | gsub("[[:cntrl:]]"; " ")) + "]") | paint($open) end) catch null),
  (try (((.workspace.current_dir | str) // (.cwd | str) | base) as $t | "" as $open | if $t == null or $t == "" then null else ("๐Ÿ“ " + ($t | gsub("[[:cntrl:]]"; " ")) + "") | paint($open) end) catch null),
  (try (($git_branch | str) as $t | "" as $open | if $t == null or $t == "" then null else ("| ๐ŸŒฟ " + ($t | gsub("[[:cntrl:]]"; " ")) + "") | paint($open) end) catch null),
  (try ((if $git_staged + $git_modified == 0 then null else [if $git_staged > 0 then "+\($git_staged)" else empty end, if $git_modified > 0 then "~\($git_modified)" else empty end] | join(" ") end) as $t | "\u001b[33m" as $open | if $t == null or $t == "" then null else ("" + ($t | gsub("[[:cntrl:]]"; " ")) + "") | paint($open) end) catch null)
] | map(select(. != null)) | join("\u001b[90m \u001b[0m") | select(length > 0)),
([
  (try ((.context_window.used_percentage | num | bar(10; "โ–ˆ"; "โ–‘")) as $t | ((.context_window.used_percentage | num) as $p | if $p == null then "" elif $p >= 90 then "\u001b[31m" elif $p >= 70 then "\u001b[33m" else "\u001b[32m" end) as $open | if $t == null or $t == "" then null else ("" + ($t | gsub("[[:cntrl:]]"; " ")) + "") | paint($open) end) catch null),
  (try ((.context_window.used_percentage | num | pct) as $t | ((.context_window.used_percentage | num) as $p | if $p == null then "" elif $p >= 90 then "\u001b[31m" elif $p >= 70 then "\u001b[33m" else "\u001b[32m" end) as $open | if $t == null or $t == "" then null else ("" + ($t | gsub("[[:cntrl:]]"; " ")) + "") | paint($open) end) catch null),
  (try ((.cost.total_cost_usd | num | usd) as $t | "\u001b[33m" as $open | if $t == null or $t == "" then null else ("" + ($t | gsub("[[:cntrl:]]"; " ")) + "") | paint($open) end) catch null),
  (try ((.cost.total_duration_ms | num | if . == null then null else . / 1000 | dur end) as $t | "" as $open | if $t == null or $t == "" then null else ("โฑ " + ($t | gsub("[[:cntrl:]]"; " ")) + "") | paint($open) end) catch null)
] | map(select(. != null)) | join("\u001b[90m | \u001b[0m") | select(length > 0))
CCPP_JQ
)
printf '%s' "$input" | jq -r --arg git_branch "$branch" --argjson git_staged "${staged:-0}" --argjson git_modified "${modified:-0}" "$program"
CCPP_STATUSLINE
chmod +x ~/.claude/statusline.sh
settings=~/.claude/settings.json
if [ -s "$settings" ]; then cp "$settings" "$settings.bak"; else echo '{}' > "$settings"; fi
jq --argjson statusLine '{"type":"command","command":"~/.claude/statusline.sh"}' '.statusLine = $statusLine' "$settings" > "$settings.tmp" && mv "$settings.tmp" "$settings"

How to use it

  1. Start from a preset, or add widgets to a line and select one to set its text, colors and thresholds.
  2. Check the preview against each sample session; edit the sample JSON to try your own values.
  3. Copy the install command into a terminal, or save the script as ~/.claude/statusline.sh and add the statusLine entry to ~/.claude/settings.json yourself.
  4. To change it later, paste the script back into Import: the layout is stored in its ccpp-layout line.

Questions

What does the script need?
bash and jq (https://jqlang.org). On Windows, Claude Code runs status line commands through Git Bash when it is installed; install jq there too. Git widgets also need git. If jq is missing, the status line says so instead of staying blank.
Why does a widget disappear in some sessions?
Claude Code leaves fields out until they exist: rate limits only for Pro and Max subscribers after the first response, the pull request only while one is open, effort only for models that support it. A widget without a value is hidden, and so is the separator next to it.
Why does my clock or countdown stop moving?
Claude Code reruns the command on events (a new message, a mode change, /compact), debounced by 300 ms. While the session is idle nothing triggers it, so time-based widgets need a refresh interval: set one in the settings of the editor.
Do the colors look the same in my terminal?
Named colors follow your terminal's theme, so they differ from the preview's palette; hex colors are 24-bit and look the same wherever true color is supported. Links use OSC 8 and are clickable only in terminals that support it (iTerm2, Kitty, WezTerm, recent VS Code).