Skip to content

Instantly share code, notes, and snippets.

@mcansky
Created July 10, 2026 14:30
Show Gist options
  • Select an option

  • Save mcansky/4149045e9af0c523290e13776ca08918 to your computer and use it in GitHub Desktop.

Select an option

Save mcansky/4149045e9af0c523290e13776ca08918 to your computer and use it in GitHub Desktop.
Claude Code statusline — Catppuccin Mocha powerline (hardened fork of tjhanley's)

Claude Code Statusline — Catppuccin Mocha Powerline (hardened)

A custom statusline for Claude Code with Catppuccin Mocha colors and Nerd Font powerline segments.

Segments (left to right): model, directory (basename), git (branch +staged ~modified, yellow when dirty), context (fill bar · % · session cost · duration), output style (non-default only), agent (--agent only), vim mode (when enabled).

Requirements

  • Claude Code CLI
  • jq, git — JSON parsing and branch/status info
  • A Nerd Font patched terminal font (e.g. JetBrainsMono Nerd Font)
  • A terminal with truecolor support
  • bash ≥ 4.2 (uses $'\uXXXX' ANSI-C escapes)

Install

# 1. Download the script (replace RAW_URL with this gist's raw statusline.sh URL)
mkdir -p ~/.claude
curl -fsSL RAW_URL -o ~/.claude/statusline.sh
chmod +x ~/.claude/statusline.sh

# 2. Add the statusLine key to ~/.claude/settings.json WITHOUT clobbering it.
#    If you have an existing settings.json, merge with jq:
tmp=$(mktemp)
jq --arg cmd "$HOME/.claude/statusline.sh" \
   '.statusLine = {"type":"command","command":$cmd,"padding":0}' \
   ~/.claude/settings.json > "$tmp" && mv "$tmp" ~/.claude/settings.json

# 3. Restart Claude Code

If you don't have a settings.json yet, create one containing just:

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

Note: use your real config dir if you've set CLAUDE_CONFIG_DIR (e.g. ~/.claude-imfiny).

Customization

  • Colors — the BG_* / FG_* truecolor sequences near the top
  • Segments — reorder/remove the blocks in the "Build line" section
  • Bar style — change FILL_CH / EMPTY_CH
  • Cache TTLCACHE_MAX_AGE (default 5s) for git refresh rate

How it works

Claude Code pipes a JSON blob to stdin on each render tick. The script extracts fields with a single jq call, caches git status to avoid lag, and prints a powerline-styled line using ANSI escape sequences.

Credits & changes

Forked from tjhanley's original gist. This variant hardens and fixes it:

  • Progress bar built by string repetition instead of tr ' ' '━' — GNU tr is byte-oriented and mangled the multibyte box-drawing char into invalid UTF-8 (bar rendered as missing/replacement glyphs on Linux).
  • Cache mtime uses stat -c %Y (GNU) first, stat -f %m (BSD) as fallback — the original's ordering silently broke the cache-age math on Linux.
  • Git cache moved from world-writable /tmp to $XDG_CACHE_HOME (user-owned), removing a predictable-temp-file / symlink vector on shared machines.
  • Output uses printf '%s' instead of '%b', so a hostile git branch name can't inject terminal escape sequences via backslash interpretation.
  • Source is plain-ASCII — colors and glyphs use $'\033' / $'\uXXXX' ANSI-C escapes instead of raw invisible bytes, so it's safe to copy-paste and grep.
  • Added a directory segment (basename only).
#!/bin/bash
# Claude Code status line — Catppuccin Mocha powerline style
# Receives JSON session data on stdin, prints a single colored line.
# Requires a Nerd Font (e.g. any Nerd Font patched terminal font).
#
# Hardened variant:
# - Colors/glyphs use ANSI-C escapes ($'\033' / $'\uXXXX') → plain-ASCII source,
# safe to copy-paste and grep (no invisible raw ESC bytes).
# - Git cache lives under $XDG_CACHE_HOME (~/.cache) instead of world-writable /tmp,
# removing the predictable-temp-file / symlink vector on shared machines.
# - Progress bar built by string repetition (not `tr`, which is byte-oriented and
# mangles multibyte box-drawing chars into invalid UTF-8).
# - Final output uses printf '%s' (not '%b'), so a hostile git branch name
# cannot inject terminal escape sequences via backslash interpretation.
#
# Dependencies: jq, git, awk, md5/md5sum
#
# Setup: wire it into your Claude Code settings.json under the "statusLine" key.
input=$(cat)
# Catppuccin Mocha — truecolor ANSI (ANSI-C quoted, ESC = \033)
BG_BLUE=$'\033[48;2;137;180;250m'
BG_GREEN=$'\033[48;2;166;227;161m'
BG_YELLOW=$'\033[48;2;249;226;175m'
BG_MAUVE=$'\033[48;2;203;166;247m'
BG_TEAL=$'\033[48;2;148;226;213m'
BG_PEACH=$'\033[48;2;250;179;135m'
BG_SAPPHIRE=$'\033[48;2;116;199;236m'
FG_BASE=$'\033[38;2;30;30;46m'
FG_DIM=$'\033[38;2;108;112;134m' # Catppuccin Mocha overlay0 — empty bar portion
# Foreground versions of segment bg colors — used for powerline arrow transitions
FG_BLUE=$'\033[38;2;137;180;250m'
FG_GREEN=$'\033[38;2;166;227;161m'
FG_YELLOW=$'\033[38;2;249;226;175m'
FG_MAUVE=$'\033[38;2;203;166;247m'
FG_TEAL=$'\033[38;2;148;226;213m'
FG_PEACH=$'\033[38;2;250;179;135m'
FG_SAPPHIRE=$'\033[38;2;116;199;236m'
BOLD=$'\033[1m'
RESET=$'\033[0m'
# Nerd Font powerline glyphs (ANSI-C \u escapes; requires bash >= 4.2)
SEP=$'\ue0b0' # right-arrow: fg=prev_bg, bg=next_bg
CAP_L=$'\ue0b6' # left rounded cap
CAP_R=$'\ue0b4' # right rounded cap
CHIP=$'\uf2db' # fa-microchip
BRANCH=$'\ue0a0' # Powerline VCS branch
ROBOT=$'\uf544' # fa-robot
# Extract all fields in one jq call (unit separator to handle empty fields)
IFS=$'\x1f' read -r MODEL DIR PCT COST VIM_MODE DURATION_MS STYLE AGENT < <(
echo "$input" | jq -r '[
(.model.display_name // "claude"),
(.workspace.current_dir // ""),
((.context_window.used_percentage // 0) | floor | tostring),
(.cost.total_cost_usd // 0 | tostring),
(.vim.mode // ""),
(.cost.total_duration_ms // 0 | tostring),
(.output_style.name // "default"),
(.agent.name // "")
] | join("\u001f")'
)
# Git status — cached to avoid lag on large repos.
# Cache under XDG cache dir (user-owned) instead of /tmp.
CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/claude-statusline"
mkdir -p "$CACHE_DIR" 2>/dev/null
CACHE_DIR_KEY=$(printf '%s' "$DIR" | md5 2>/dev/null || printf '%s' "$DIR" | md5sum 2>/dev/null | cut -d' ' -f1)
CACHE_FILE="${CACHE_DIR}/git-${CACHE_DIR_KEY}"
CACHE_MAX_AGE=5 # seconds
cache_is_stale() {
[ ! -f "$CACHE_FILE" ] && return 0
# GNU stat (Linux) first, BSD/macOS stat as fallback
local mtime=$(stat -c %Y "$CACHE_FILE" 2>/dev/null || stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0)
local age=$(( $(date +%s) - mtime ))
[ "$age" -gt "$CACHE_MAX_AGE" ]
}
if cache_is_stale; then
if [ -n "$DIR" ] && git -C "$DIR" rev-parse --git-dir > /dev/null 2>&1; then
BRANCH_NAME=$(git -C "$DIR" branch --show-current 2>/dev/null)
STAGED=$(git -C "$DIR" diff --cached --numstat 2>/dev/null | wc -l | tr -d ' ')
MODIFIED=$(git -C "$DIR" diff --numstat 2>/dev/null | wc -l | tr -d ' ')
printf '1|%s|%s|%s\n' "$BRANCH_NAME" "$STAGED" "$MODIFIED" > "$CACHE_FILE"
else
printf '0|||\n' > "$CACHE_FILE"
fi
fi
IFS='|' read -r IS_GIT BRANCH_NAME STAGED MODIFIED < "$CACHE_FILE"
# Context bar — heavy for filled, light for empty.
# Build by string repetition (NOT `tr`, which is byte-oriented and mangles
# multibyte box-drawing chars into invalid UTF-8).
FILL_CH=$'\u2501' # heavy horizontal ━ (filled)
EMPTY_CH=$'\u2500' # light horizontal ─ (empty)
FILLED=$((PCT * 10 / 100))
EMPTY=$((10 - FILLED))
BAR=""
if [ "$FILLED" -gt 0 ]; then
BAR="${FG_BASE}"
for ((i = 0; i < FILLED; i++)); do BAR="${BAR}${FILL_CH}"; done
fi
if [ "$EMPTY" -gt 0 ]; then
BAR="${BAR}${FG_DIM}"
for ((i = 0; i < EMPTY; i++)); do BAR="${BAR}${EMPTY_CH}"; done
fi
BAR="${BAR}${FG_BASE}"
# Cost and duration formatting
COST_FMT=$(awk -v c="$COST" 'BEGIN { printf "$%.2f\n", c+0 }')
DURATION_FMT=$(awk -v ms="$DURATION_MS" 'BEGIN {
s = int(ms / 1000); m = int(s / 60); h = int(m / 60)
if (h > 0) printf "%dh%dm", h, m % 60
else printf "%dm", m
}')
# Determine git bg/fg colors based on dirty state
GIT_BG="$BG_GREEN"; GIT_FG="$FG_GREEN"
if [ "${IS_GIT:-0}" = "1" ]; then
GIT_DIRTY=0
[ "${STAGED:-0}" -gt 0 ] || [ "${MODIFIED:-0}" -gt 0 ] && GIT_DIRTY=1
[ "$GIT_DIRTY" = "1" ] && GIT_BG="$BG_YELLOW" && GIT_FG="$FG_YELLOW"
fi
# Determine vim bg/fg colors
VIM_BG="$BG_GREEN"; VIM_FG="$FG_GREEN"
[ "$VIM_MODE" = "NORMAL" ] && VIM_BG="$BG_YELLOW" && VIM_FG="$FG_YELLOW"
# Build line — LAST_FG tracks the previous segment's bg color for the right cap
LINE="${RESET}${FG_BLUE}${CAP_L}${BG_BLUE}${FG_BASE}${BOLD} ${CHIP} ${MODEL} "
LAST_FG="$FG_BLUE"
# Directory — sapphire pill, basename of current dir only
DIR_BASE=${DIR##*/}
if [ -n "$DIR_BASE" ]; then
LINE="${LINE}${LAST_FG}${BG_SAPPHIRE}${SEP}${FG_BASE}${BOLD} ${DIR_BASE} "
LAST_FG="$FG_SAPPHIRE"
fi
if [ "${IS_GIT:-0}" = "1" ]; then
GIT_TEXT="${BRANCH} ${BRANCH_NAME}"
[ "${STAGED:-0}" -gt 0 ] && GIT_TEXT="${GIT_TEXT} +${STAGED}"
[ "${MODIFIED:-0}" -gt 0 ] && GIT_TEXT="${GIT_TEXT} ~${MODIFIED}"
LINE="${LINE}${LAST_FG}${GIT_BG}${SEP}${FG_BASE}${BOLD} ${GIT_TEXT} "
LAST_FG="$GIT_FG"
fi
# Context + cost + duration segment
LINE="${LINE}${LAST_FG}${BG_MAUVE}${SEP}${FG_BASE}${BOLD} ${BAR} ${PCT}% ${COST_FMT} ${DURATION_FMT} "
LAST_FG="$FG_MAUVE"
# Output style — teal pill, hidden when default
if [ -n "$STYLE" ] && [ "$STYLE" != "default" ]; then
LINE="${LINE}${LAST_FG}${BG_TEAL}${SEP}${FG_BASE}${BOLD} ${STYLE} "
LAST_FG="$FG_TEAL"
fi
# Agent — peach pill, only shown when --agent flag is active
if [ -n "$AGENT" ]; then
LINE="${LINE}${LAST_FG}${BG_PEACH}${SEP}${FG_BASE}${BOLD} ${ROBOT} ${AGENT} "
LAST_FG="$FG_PEACH"
fi
# Vim mode — only shown when vim mode is enabled
if [ -n "$VIM_MODE" ]; then
LINE="${LINE}${LAST_FG}${VIM_BG}${SEP}${FG_BASE}${BOLD} ${VIM_MODE} "
LAST_FG="$VIM_FG"
fi
# Right rounded cap
LINE="${LINE}${RESET}${LAST_FG}${CAP_R}${RESET}"
printf '%s\n' "$LINE"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment