Skip to content

Instantly share code, notes, and snippets.

@nazt
Last active March 7, 2026 05:55
Show Gist options
  • Select an option

  • Save nazt/82e6b9b82056e36e2a0298202c03a7b8 to your computer and use it in GitHub Desktop.

Select an option

Save nazt/82e6b9b82056e36e2a0298202c03a7b8 to your computer and use it in GitHub Desktop.
Starship-inspired status line for Claude Code CLI
local wezterm = require 'wezterm'
local act = wezterm.action
local config = wezterm.config_builder()
-- Font: Nerd Font for starship icons
config.font_size = 13.0
-- Catppuccin Mocha (matches starship palette)
config.color_scheme = 'Catppuccin Mocha'
config.colors = {
background = '#0a0e14',
}
config.hyperlink_rules = wezterm.default_hyperlink_rules()
-- Remote white.local paths β†’ local sshfs mounts
table.insert(config.hyperlink_rules, {
regex = [[/home/nat/([^ \t:),'"'`]+)]],
format = 'file:///opt/white/$1',
})
table.insert(config.hyperlink_rules, {
regex = [[/home/openclaw/([^ \t:),'"'`]+)]],
format = 'file:///opt/openclaw/$1',
})
-- Local absolute paths
table.insert(config.hyperlink_rules, {
regex = [[(/Users/[^ \t:),'"'`]+)]],
format = 'file://$1',
})
-- ~/path style
table.insert(config.hyperlink_rules, {
regex = [[~/([^ \t:),'"'`]+)]],
format = 'file:///Users/nat/$1',
})
-- Relative paths β€” ./path and ψ/path (resolved via OSC 7 cwd on click)
table.insert(config.hyperlink_rules, {
regex = [[(\./[^ \t:),'"'`]+)]],
format = 'rel:$1',
})
table.insert(config.hyperlink_rules, {
regex = [[(?<!/)(ψ/[^ \t:),'"'`]+)]],
format = 'psi:$1',
})
-- Rewrite remote cwd to local sshfs path
local function resolve_cwd(pane)
local cwd = pane:get_current_working_dir()
if not cwd then return nil end
local p = cwd.file_path or ''
p = p:gsub('^/home/openclaw/', '/opt/openclaw/')
p = p:gsub('^/home/nat/', '/opt/white/')
return p
end
-- Intercept link clicks to resolve relative paths using OSC 7 cwd
wezterm.on('open-uri', function(window, pane, uri)
-- psi: and rel: schemes β€” resolve relative to cwd
local rel = uri:match('^psi:(.+)$') or uri:match('^rel:(.+)$')
if rel then
rel = rel:gsub('^%./', '') -- strip leading ./
local cwd_path = resolve_cwd(pane)
if cwd_path then
wezterm.open_with(cwd_path .. '/' .. rel)
end
return false
end
-- file:// URIs β€” force open with TextEdit if macOS doesn't know the type
local path = uri:match('^file://(.+)$')
if path then
local success = wezterm.run_child_process({ '/usr/bin/open', path })
if not success then
wezterm.run_child_process({ '/usr/bin/open', '-a', 'TextEdit', path })
end
return false
end
end)
-- Smart CMD+V: image in clipboard β†’ save to sshfs + paste path, else normal paste
config.keys = {
{
key = 'v',
mods = 'CMD',
action = wezterm.action_callback(function(window, pane)
local success, stdout = wezterm.run_child_process({
'/bin/bash', '-c',
'f="/opt/white/Code/tmp/clipboard/$(date +%Y%m%d-%H%M%S).png"; '
.. 'mkdir -p /opt/white/Code/tmp/clipboard 2>/dev/null; '
.. '/opt/homebrew/bin/pngpaste "$f" 2>/dev/null && '
.. 'echo "/home/nat/Code/tmp/clipboard/$(basename $f)"'
})
if success and stdout and stdout:match('%.png') then
pane:send_text(stdout:gsub('%s+$', ''))
else
window:perform_action(act.PasteFrom 'Clipboard', pane)
end
end),
},
}
-- iTerm2 style: CMD+Click opens links, plain click selects text
config.mouse_bindings = {
{
event = { Up = { streak = 1, button = 'Left' } },
mods = 'NONE',
action = act.CompleteSelection 'ClipboardAndPrimarySelection',
},
{
event = { Up = { streak = 1, button = 'Left' } },
mods = 'CMD',
action = act.OpenLinkAtMouseCursor,
},
{
event = { Down = { streak = 1, button = 'Left' } },
mods = 'CMD',
action = act.Nop,
},
}
return config

Building a Starship-Inspired Status Line for Claude Code

Claude Code has a customizable status line at the bottom of the terminal. By default it shows basic info like the model and token count, but you can make it much more useful with a custom script.

Here's what we built β€” a Starship-inspired status line with icons, git info, context usage, and auto-compact status:

πŸ• 12:35 πŸ“‚ ~/my-project on  main* β€’ πŸ€– Opus 4.6 β€’ πŸ“Š 28% (56k/160k) β€’ πŸ”„ auto-compact

When auto-compact is enabled, the max shows the effective limit (80% of 200k = 160k) since that's when compaction triggers. With auto-compact off, you get the full window:

πŸ• 12:35 πŸ“‚ ~/my-project on  main* β€’ πŸ€– Opus 4.6 β€’ πŸ“Š 28% (56k/200k) β€’ ❌ auto-compact

When the terminal is narrow, it automatically splits into two lines:

πŸ• 12:35 πŸ“‚ ~/my-project on  main* β€’ πŸ€– Opus 4.6
πŸ“Š 28% (56k/160k) β€’ πŸ”„ auto-compact

Setup

1. Create the script

Save this as ~/.claude/statusline-command.sh:

#!/bin/bash

# Claude Code status line β€” Starship-inspired with icons
# Receives JSON on stdin from Claude Code

input=$(cat)

# Extract JSON data
cwd=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // "~"')
model=$(echo "$input" | jq -r '.model.display_name // .model.id // "Claude"')
pct=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
used_k=$(echo "$input" | jq -r '((.context_window.current_usage | .input_tokens + .cache_creation_input_tokens + .cache_read_input_tokens + .output_tokens) // 0) / 1000 | floor')
max_k=$(echo "$input" | jq -r '(.context_window.context_window_size // 0) / 1000 | floor')

# Format directory (~ for home)
if [[ "$cwd" == "$HOME"* ]]; then
  display_dir="${cwd/#$HOME/~}"
else
  display_dir="$cwd"
fi

# Git branch
git_info=""
if git -C "$cwd" rev-parse --git-dir >/dev/null 2>&1; then
  branch=$(git -C "$cwd" symbolic-ref --short HEAD 2>/dev/null || git -C "$cwd" rev-parse --short HEAD 2>/dev/null)
  if [ -n "$branch" ]; then
    dirty=""
    if ! git -C "$cwd" diff-index --quiet HEAD -- 2>/dev/null; then
      dirty="*"
    fi
    git_info=" on  ${branch}${dirty}"
  fi
fi

# Auto-compact: key only exists in ~/.claude.json when disabled (false)
# Absent = enabled (default), false = disabled
# When enabled, triggers at ~80% so effective max is 80% of context window
compact_val=$(jq -r 'if .autoCompactEnabled == false then "false" else "true" end' ~/.claude.json 2>/dev/null)
compact_pct=${CLAUDE_AUTOCOMPACT_PCT_OVERRIDE:-80}
if [ "$compact_val" = "false" ]; then
  compact_icon="❌ auto-compact"
else
  compact_icon="πŸ”„ auto-compact"
  max_k=$((max_k * compact_pct / 100))
fi

# Time
now=$(date '+%H:%M')

# Build one-line version, split to two if too long
line1=$(printf 'πŸ• %s πŸ“‚ %s%s β€’ πŸ€– %s' "$now" "$display_dir" "$git_info" "$model")
line2=$(printf 'πŸ“Š %s%% (%sk/%sk) β€’ %s' "$pct" "$used_k" "$max_k" "$compact_icon")
oneline="$line1 β€’ $line2"

# Get terminal width (fallback 80)
cols=${COLUMNS:-$(tput cols 2>/dev/null || echo 80)}

# If one line fits, use it; otherwise split
if [ ${#oneline} -le "$cols" ]; then
  printf '%s' "$oneline"
else
  printf '%s\n%s' "$line1" "$line2"
fi

2. Make it executable

chmod +x ~/.claude/statusline-command.sh

3. Add to settings

Add the statusLine field to ~/.claude/settings.json:

{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline-command.sh"
  }
}

That's it! The status line updates after each interaction.

What it shows

Icon Info Source
πŸ• Current time date
πŸ“‚ Working directory (with ~) .workspace.current_dir
Git branch + dirty indicator git symbolic-ref
πŸ€– Model name .model.display_name
πŸ“Š Context usage % and tokens (effective max) .context_window
πŸ”„/❌ Auto-compact on/off ~/.claude.json

Effective context window

When auto-compact is enabled, it triggers at ~80% of the context window. This means you never actually get to use the full 200k β€” compaction kicks in at ~160k. The script adjusts the displayed max to reflect this reality:

  • Auto-compact on: πŸ“Š 35% (56k/160k) β€” shows effective usable limit
  • Auto-compact off: πŸ“Š 28% (56k/200k) β€” shows full context window

You can override the threshold with CLAUDE_AUTOCOMPACT_PCT_OVERRIDE env var (default: 80).

How the JSON input works

Claude Code pipes JSON to your script's stdin on every interaction. Key fields:

{
  "model": { "id": "claude-opus-4-6", "display_name": "Opus 4.6" },
  "workspace": { "current_dir": "/home/user/project" },
  "context_window": {
    "used_percentage": 28,
    "context_window_size": 200000,
    "current_usage": {
      "input_tokens": 3,
      "output_tokens": 94,
      "cache_creation_input_tokens": 372,
      "cache_read_input_tokens": 55664
    }
  },
  "cost": { "total_cost_usd": 1.55 },
  "version": "2.1.71"
}

The auto-compact discovery

The trickiest part was showing auto-compact status. It's not in the statusLine JSON input. After digging through the Claude Code binary with strings, we found:

  • autoCompactEnabled is read from ~/.claude.json (home directory root, not ~/.claude/.claude.json)
  • The key only exists when set to false β€” when enabled (default), it's absent
  • jq's // operator treats false as falsy, so you need if .autoCompactEnabled == false instead of .autoCompactEnabled // true

Requirements

  • jq β€” for JSON parsing
  • git β€” for branch detection
  • A font with emoji support (most modern terminals)

Customization ideas

  • Add cost.total_cost_usd to show session spend
  • Add cost.total_duration_ms for session duration
  • Add vim.mode if you use vim mode
  • Add worktree.name if you use worktrees
  • Change icons to match your terminal theme

Built with Claude Code on a lazy Saturday afternoon.

#!/bin/bash
# Claude Code status line β€” Starship-inspired with icons
# Receives JSON on stdin from Claude Code
input=$(cat)
# Extract JSON data
cwd=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // "~"')
model=$(echo "$input" | jq -r '.model.display_name // .model.id // "Claude"')
pct=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
used_k=$(echo "$input" | jq -r '((.context_window.current_usage | .input_tokens + .cache_creation_input_tokens + .cache_read_input_tokens + .output_tokens) // 0) / 1000 | floor')
max_k=$(echo "$input" | jq -r '(.context_window.context_window_size // 0) / 1000 | floor')
# Format directory (~ for home)
if [[ "$cwd" == "$HOME"* ]]; then
display_dir="${cwd/#$HOME/~}"
else
display_dir="$cwd"
fi
# Git branch + worktree
git_info=""
if git -C "$cwd" rev-parse --git-dir >/dev/null 2>&1; then
branch=$(git -C "$cwd" symbolic-ref --short HEAD 2>/dev/null || git -C "$cwd" rev-parse --short HEAD 2>/dev/null)
if [ -n "$branch" ]; then
dirty=""
if ! git -C "$cwd" diff-index --quiet HEAD -- 2>/dev/null; then
dirty="*"
fi
# Check if in a worktree (git dir is a file pointing to main repo)
wt=""
git_dir=$(git -C "$cwd" rev-parse --git-dir 2>/dev/null)
if [ -f "$git_dir" ] || echo "$input" | jq -e '.worktree' >/dev/null 2>&1; then
wt_name=$(echo "$input" | jq -r '.worktree.name // empty' 2>/dev/null)
[ -z "$wt_name" ] && wt_name=$(basename "$cwd")
wt=" 🌳 ${wt_name}"
fi
git_info=" on ${branch}${dirty}${wt}"
fi
fi
# Auto-compact: key only exists in ~/.claude.json when disabled (false)
# Absent = enabled (default), false = disabled
# When enabled, triggers at ~80% so effective max is 80% of context window
compact_val=$(jq -r 'if .autoCompactEnabled == false then "false" else "true" end' ~/.claude.json 2>/dev/null)
compact_pct=${CLAUDE_AUTOCOMPACT_PCT_OVERRIDE:-80}
if [ "$compact_val" = "false" ]; then
compact_icon="❌ auto-compact"
else
compact_icon="πŸ”„ auto-compact"
max_k=$((max_k * compact_pct / 100))
# Recalculate pct against effective max
if [ "$max_k" -gt 0 ]; then
pct=$((used_k * 100 / max_k))
fi
fi
# Time
now=$(date '+%H:%M')
# Build one-line version, split to two if too long
line1=$(printf 'πŸ• %s πŸ“‚ %s%s β€’ πŸ€– %s' "$now" "$display_dir" "$git_info" "$model")
line2=$(printf 'πŸ“Š %s%% (%sk/%sk) β€’ %s' "$pct" "$used_k" "$max_k" "$compact_icon")
oneline="$line1 β€’ $line2"
# Get terminal width (fallback 80)
cols=${COLUMNS:-$(tput cols 2>/dev/null || echo 80)}
# If one line fits, use it; otherwise split
if [ ${#oneline} -le "$cols" ]; then
printf '%s' "$oneline"
else
printf '%s\n%s' "$line1" "$line2"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment