Skip to content

Instantly share code, notes, and snippets.

@enimiste
Last active July 17, 2026 19:56
Show Gist options
  • Select an option

  • Save enimiste/b4b33bd45e2d8e5463386524e3d8bd8f to your computer and use it in GitHub Desktop.

Select an option

Save enimiste/b4b33bd45e2d8e5463386524e3d8bd8f to your computer and use it in GitHub Desktop.
Claude code status line shell script (to be put inside .claude folder + configure it in the .claude/settings.json file)
#!/bin/sh
input=$(cat)
# ---------------------------------------------------------------------------
# Options CLI (défauts : deux lignes, organisation affichée)
# ---------------------------------------------------------------------------
layout="two" # two | one
show_org="yes" # yes | no
path_mode="full" # full | short
for arg in "$@"; do
case "$arg" in
--one-line|--single-line) layout="one" ;;
--two-lines|--two-line) layout="two" ;;
--no-org|--hide-org) show_org="no" ;;
--org|--show-org) show_org="yes" ;;
--full-path|--long-path) path_mode="full" ;;
--short-path|--basename) path_mode="short" ;;
esac
done
# ---------------------------------------------------------------------------
# Colors (ANSI) — use $'...' so \033 is interpreted by the shell
# ---------------------------------------------------------------------------
RESET=$'\033[0m'
BOLD=$'\033[1m'
CYAN=$'\033[36m'
YELLOW=$'\033[33m'
GREEN=$'\033[32m'
BLUE=$'\033[34m'
MAGENTA=$'\033[35m'
RED=$'\033[31m'
GRAY=$'\033[90m'
WHITE=$'\033[97m'
# ---------------------------------------------------------------------------
# 1. Model name
# ---------------------------------------------------------------------------
model=$(echo "$input" | jq -r '.model.display_name // empty')
# ---------------------------------------------------------------------------
# 2. Effort level — from JSON first, fall back to settings.json
# ---------------------------------------------------------------------------
effort=$(echo "$input" | jq -r '.effort.level // empty')
if [ -z "$effort" ]; then
script_dir="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
settings_file=""
if [ -f "$script_dir/settings.json" ]; then
settings_file="$script_dir/settings.json"
elif [ -f "$HOME/.claude/settings.json" ]; then
settings_file="$HOME/.claude/settings.json"
fi
if [ -n "$settings_file" ]; then
effort=$(jq -r '.effortLevel // empty' "$settings_file" 2>/dev/null)
fi
fi
case "$effort" in
low) effort_label="low" ; effort_color="$GRAY" ;;
medium) effort_label="medium" ; effort_color="$YELLOW" ;;
high) effort_label="high" ; effort_color="$GREEN" ;;
xhigh) effort_label="xhigh" ; effort_color="$CYAN" ;;
max) effort_label="max" ; effort_color="$MAGENTA" ;;
*) effort_label="" ; effort_color="$GRAY" ;;
esac
# ---------------------------------------------------------------------------
# 3. Git branch — run in the cwd reported by Claude Code
# ---------------------------------------------------------------------------
cwd=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // empty' | grep -v '^$')
[ -z "$cwd" ] && cwd="$(pwd)"
git_branch=""
if command -v git >/dev/null 2>&1; then
git_branch=$(git -C "$cwd" --no-optional-locks branch --show-current 2>/dev/null)
if [ -z "$git_branch" ]; then
git_branch=$(git -C "$cwd" --no-optional-locks rev-parse --short HEAD 2>/dev/null)
[ -n "$git_branch" ] && git_branch="(${git_branch})"
fi
fi
# ---------------------------------------------------------------------------
# 4. Current directory (path complet + basename ; l'affichage dépend du layout)
# ---------------------------------------------------------------------------
current_dir_path=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // empty' | grep -v '^$')
[ -z "$current_dir_path" ] && current_dir_path="$(pwd)"
current_dir_name=$(basename "$current_dir_path")
# ---------------------------------------------------------------------------
# 4b. Nombre d'échanges (prompts utilisateur) dans la session
# ---------------------------------------------------------------------------
transcript=$(echo "$input" | jq -r '.transcript_path // empty')
msg_count=""
if [ -n "$transcript" ] && [ -f "$transcript" ]; then
msg_count=$(jq -rc 'select(.type=="user" and (.isMeta != true) and ((.message.content|type)=="string" or (.message.content[0].type != "tool_result")))' "$transcript" 2>/dev/null | wc -l | tr -d ' ')
resp_count=$(jq -rc 'select(.type=="assistant") | .message.id // empty' "$transcript" 2>/dev/null | sort -u | grep -c .)
fi
# ---------------------------------------------------------------------------
# 5. Context usage (pourcentage coloré + token counts)
# ---------------------------------------------------------------------------
used_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty' | grep -v '^null$')
bar=""
pct_str=""
token_str=""
bar_color="$GRAY"
if [ -n "$used_pct" ]; then
# 2 decimal places for the percentage
pct_str=$(awk "BEGIN {printf \"%.2f%%\", $used_pct}")
pct_int=$(printf "%.0f" "$used_pct")
# (plus de barre ████░░░░ — le code couleur est reporté sur le pourcentage)
if [ "$pct_int" -ge 80 ]; then
bar_color="$RED"
elif [ "$pct_int" -ge 50 ]; then
bar_color="$YELLOW"
else
bar_color="$GREEN"
fi
# Taille max de la fenêtre de contexte (sans le détail des tokens utilisés)
win_size=$(echo "$input" | jq -r '.context_window.context_window_size // empty' | grep -v '^null$')
if [ -n "$win_size" ] && [ "$win_size" -gt 0 ] 2>/dev/null; then
total_k=$(awk "BEGIN {printf \"%.1fk\", $win_size/1000}")
token_str="(${total_k})"
fi
else
# Avant le premier échange : aucun token réel n'est encore compté côté API.
# On laisse bar / pct_str / token_str vides pour masquer le segment "context".
:
fi
# ---------------------------------------------------------------------------
# 5a. Rate limits Claude.ai (session 5h + hebdo 7j) — depuis le stdin only,
# aucun appel réseau. Champs absents (ancienne version / juste après
# /clear) => segments masqués silencieusement.
# ---------------------------------------------------------------------------
now_epoch=$(date +%s)
# Formate un delta de secondes en "Xj YhZZm" (jours omis si 0, heures omises si 0, "0m" si passé)
fmt_countdown() {
d="$1"
if [ -z "$d" ] || [ "$d" -le 0 ] 2>/dev/null; then
printf '0m'
return
fi
days=$((d / 86400))
h=$(((d % 86400) / 3600))
m=$(((d % 3600) / 60))
if [ "$days" -gt 0 ]; then
printf '%dj%dh%02dm' "$days" "$h" "$m"
elif [ "$h" -gt 0 ]; then
printf '%dh%02dm' "$h" "$m"
else
printf '%dm' "$m"
fi
}
# Formate un timestamp epoch en "jour heure:min" lisible (macOS + Linux)
fmt_datetime() {
ts="$1"
# GNU date (Linux)
out=$(date -d "@$ts" '+%a %H:%M' 2>/dev/null)
if [ -z "$out" ]; then
# BSD date (macOS)
out=$(date -r "$ts" '+%a %H:%M' 2>/dev/null)
fi
printf '%s' "$out"
}
pct_color_for() {
p="$1"
pi=$(printf '%.0f' "$p")
if [ "$pi" -ge 80 ]; then
printf '%s' "$RED"
elif [ "$pi" -ge 50 ]; then
printf '%s' "$YELLOW"
else
printf '%s' "$GREEN"
fi
}
# -- Session 5h --------------------------------------------------------------
five_hour_pct=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty' | grep -v '^null$')
five_hour_reset=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty' | grep -v '^null$')
five_hour_str=""
if [ -n "$five_hour_pct" ]; then
five_hour_color=$(pct_color_for "$five_hour_pct")
five_hour_pct_str=$(printf '%.0f' "$five_hour_pct")
five_hour_str="${five_hour_color}${five_hour_pct_str}%${RESET}"
if [ -n "$five_hour_reset" ]; then
five_hour_left=$(fmt_countdown $((five_hour_reset - now_epoch)))
five_hour_str="${five_hour_str} ${GRAY}(${five_hour_left})${RESET}"
fi
fi
# -- Hebdo 7j -----------------------------------------------------------------
seven_day_pct=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty' | grep -v '^null$')
seven_day_reset=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty' | grep -v '^null$')
seven_day_str=""
if [ -n "$seven_day_pct" ]; then
seven_day_color=$(pct_color_for "$seven_day_pct")
seven_day_pct_str=$(printf '%.0f' "$seven_day_pct")
seven_day_str="📅 ${seven_day_color}${seven_day_pct_str}%${RESET}"
if [ -n "$seven_day_reset" ]; then
seven_day_left=$(fmt_countdown $((seven_day_reset - now_epoch)))
seven_day_str="${seven_day_str} ${GRAY}(${seven_day_left})${RESET}"
fi
fi
# ---------------------------------------------------------------------------
# 6. Organisation + méthode de login (dépendent de la session, pas du stdin)
# ---------------------------------------------------------------------------
# Résolution du .claude.json de la session (respecte CLAUDE_CONFIG_DIR ;
# attention : sans la variable le fichier est $HOME/.claude.json, PAS
# $HOME/.claude/.claude.json).
claude_json=""
if [ -n "$CLAUDE_CONFIG_DIR" ] && [ -f "$CLAUDE_CONFIG_DIR/.claude.json" ]; then
claude_json="$CLAUDE_CONFIG_DIR/.claude.json"
elif [ -f "$HOME/.claude.json" ]; then
claude_json="$HOME/.claude.json"
fi
org_name=""
org_type=""
if [ -n "$claude_json" ]; then
org_name=$(jq -r '.oauthAccount.organizationName // empty' "$claude_json" 2>/dev/null)
org_type=$(jq -r '.oauthAccount.organizationType // empty' "$claude_json" 2>/dev/null)
fi
# Méthode de login (ordre de précédence officiel Claude Code)
if [ -n "$CLAUDE_CODE_USE_BEDROCK" ]; then login_label="bedrock"
elif [ -n "$CLAUDE_CODE_USE_VERTEX" ]; then login_label="vertex"
elif [ -n "$CLAUDE_CODE_USE_FOUNDRY" ]; then login_label="foundry"
elif [ -n "$ANTHROPIC_AUTH_TOKEN" ]; then login_label="gateway"
elif [ -n "$ANTHROPIC_API_KEY" ]; then login_label="api-key"
elif [ -n "$CLAUDE_CODE_OAUTH_TOKEN" ]; then login_label="oauth-token"
elif [ -n "$org_name" ]; then login_label="oauth"
else login_label="?"
fi
# ---------------------------------------------------------------------------
# Assemble the status line (layout: two|one)
# Ligne 1: 🤖 model | 💪 effort | 💬 counter | 🪟 context | ⏳ 5h | 📅 7j
# Ligne 2: 🌿 branch | 📁 dir | 🏢 org | 🔑 login
# (en mode --one-line, les deux groupes sont concaténés sur une seule ligne)
# ---------------------------------------------------------------------------
SEP="${GRAY} | ${RESET}"
out="${BOLD}${BLUE}🤖 ${model}${RESET}"
[ -n "$effort_label" ] && out="${out}${SEP}💪 ${effort_color}${effort_label}${RESET}"
# Compteur directionnel (↑ prompts / ↓ réponses) — masqué avant le 1er prompt
if [ -n "$msg_count" ] && [ "$msg_count" -gt 0 ] 2>/dev/null; then
out="${out}${SEP}${CYAN}💬 ${msg_count}${resp_count:-0}${RESET}"
fi
# Context — masqué tant qu'aucune donnée réelle n'est disponible (avant 1er échange)
if [ -n "$used_pct" ]; then
out="${out}${SEP}🪟 ${bar_color}${pct_str}${RESET}"
[ -n "$token_str" ] && out="${out} ${GRAY}${token_str}${RESET}"
fi
# Rate limits Claude.ai (session 5h + hebdo 7j) — juste après le context,
# masqués si absents du stdin
[ -n "$five_hour_str" ] && out="${out}${SEP}${five_hour_str}"
[ -n "$seven_day_str" ] && out="${out}${SEP}${seven_day_str}"
# --- Ligne 2 : branche | dossier | organisation | login ---------------------
# Helper : ajoute un segment à line2, sans séparateur en tête du 1er segment.
line2=""
add2() {
if [ -z "$line2" ]; then
line2="$1"
else
line2="${line2}${SEP}$1"
fi
}
# Git branch en tête de la 2e ligne
[ -n "$git_branch" ] && add2 "${GREEN}🌿 ${git_branch}${RESET}"
# Dossier courant : path absolu complet (--full-path, défaut) ou basename
# (--short-path) — indépendant du layout
if [ "$path_mode" = "short" ]; then
dir_display="$current_dir_name"
else
dir_display="$current_dir_path"
fi
[ -n "$dir_display" ] && add2 "${CYAN}📁 ${dir_display}${RESET}"
# Organisation (nom + type) juste après le dossier — masquée si --no-org
if [ "$show_org" = "yes" ] && [ -n "$org_name" ]; then
org_disp="$org_name"
[ -n "$org_type" ] && org_disp="${org_disp} (${org_type})"
add2 "${MAGENTA}🏢 ${org_disp}${RESET}"
fi
# Méthode de login juste après l'organisation
add2 "${CYAN}🔑 ${login_label}${RESET}"
# Affichage selon le layout
if [ "$layout" = "one" ]; then
# Tout sur une seule ligne : ligne 1 + ligne 2 concaténées
full="$out"
[ -n "$line2" ] && full="${full}${SEP}${line2}"
printf '%s\n' "$full"
else
# Deux lignes : ligne 1 puis ligne 2 (seulement si non vide)
if [ -n "$line2" ]; then
printf '%s\n%s\n' "$out" "$line2"
else
printf '%s\n' "$out"
fi
fi
#!/usr/bin/env node
'use strict';
// ---------------------------------------------------------------------------
// Claude Code status line (Windows / cross-platform Node.js port)
// Faithful port of statusline-command.sh — no jq / coreutils dependency.
// Order: model | 💪 effort | 💬 counter | 🪟 context | ⏳ 5h | 📅 7j | 🌿 branch | 📁 dir | 🏢 org | 🔑 login
// ---------------------------------------------------------------------------
const fs = require('fs');
const path = require('path');
const os = require('os');
const { execFileSync } = require('child_process');
// --- Colors (ANSI) ---------------------------------------------------------
const RESET = '\x1b[0m';
const BOLD = '\x1b[1m';
const CYAN = '\x1b[36m';
const YELLOW = '\x1b[33m';
const GREEN = '\x1b[32m';
const BLUE = '\x1b[34m';
const MAGENTA = '\x1b[35m';
const RED = '\x1b[31m';
const GRAY = '\x1b[90m';
// --- Read + parse stdin JSON -----------------------------------------------
let input = {};
try {
const raw = fs.readFileSync(0, 'utf8');
input = raw.trim() ? JSON.parse(raw) : {};
} catch (_) {
input = {};
}
const get = (obj, pathStr) => {
return pathStr.split('.').reduce((o, k) => (o == null ? undefined : o[k]), obj);
};
const readJson = (file) => {
try {
return JSON.parse(fs.readFileSync(file, 'utf8'));
} catch (_) {
return null;
}
};
// --- 1. Model name ----------------------------------------------------------
const model = get(input, 'model.display_name') || '';
// --- 2. Effort level (JSON first, then settings.json) -----------------------
let effort = get(input, 'effort.level') || '';
if (!effort) {
const candidates = [
path.join(__dirname, 'settings.json'),
path.join(os.homedir(), '.claude', 'settings.json'),
];
for (const f of candidates) {
if (fs.existsSync(f)) {
const s = readJson(f);
if (s && s.effortLevel) { effort = s.effortLevel; break; }
}
}
}
let effortLabel = '';
let effortColor = GRAY;
switch (effort) {
case 'low': effortLabel = 'low'; effortColor = GRAY; break;
case 'medium': effortLabel = 'medium'; effortColor = YELLOW; break;
case 'high': effortLabel = 'high'; effortColor = GREEN; break;
case 'xhigh': effortLabel = 'xhigh'; effortColor = CYAN; break;
case 'max': effortLabel = 'max'; effortColor = MAGENTA; break;
}
// --- 3. Directory + git branch ---------------------------------------------
const cwd = get(input, 'workspace.current_dir') || get(input, 'cwd') || process.cwd();
let gitBranch = '';
try {
gitBranch = execFileSync(
'git', ['-C', cwd, '--no-optional-locks', 'branch', '--show-current'],
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
).trim();
if (!gitBranch) {
const short = execFileSync(
'git', ['-C', cwd, '--no-optional-locks', 'rev-parse', '--short', 'HEAD'],
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
).trim();
if (short) gitBranch = `(${short})`;
}
} catch (_) { /* not a repo / no git */ }
const currentDirName = path.basename(cwd);
// --- 4b. Session exchange counts (↑ prompts / ↓ responses) ------------------
let msgCount = 0;
let respCount = 0;
const transcript = get(input, 'transcript_path') || '';
if (transcript && fs.existsSync(transcript)) {
try {
const lines = fs.readFileSync(transcript, 'utf8').split('\n');
const respIds = new Set();
for (const line of lines) {
if (!line.trim()) continue;
let e;
try { e = JSON.parse(line); } catch (_) { continue; }
if (e.type === 'user' && e.isMeta !== true) {
const content = e.message && e.message.content;
const isToolResult =
Array.isArray(content) && content[0] && content[0].type === 'tool_result';
if (typeof content === 'string' || !isToolResult) msgCount++;
} else if (e.type === 'assistant' && e.message && e.message.id) {
respIds.add(e.message.id);
}
}
respCount = respIds.size;
} catch (_) { /* ignore */ }
}
// --- 5. Context usage -------------------------------------------------------
const usedPct = get(input, 'context_window.used_percentage');
let pctStr = '';
let tokenStr = '';
let barColor = GRAY;
const hasPct = usedPct !== undefined && usedPct !== null;
if (hasPct) {
pctStr = `${Number(usedPct).toFixed(2)}%`;
const pctInt = Math.round(Number(usedPct));
if (pctInt >= 80) barColor = RED;
else if (pctInt >= 50) barColor = YELLOW;
else barColor = GREEN;
// Taille max de la fenêtre de contexte (sans le détail des tokens utilisés)
const winSize = get(input, 'context_window.context_window_size');
if (winSize != null && Number(winSize) > 0) {
const totalK = `${(Number(winSize) / 1000).toFixed(1)}k`;
tokenStr = `(${totalK})`;
}
}
// --- 5a. Rate limits Claude.ai (session 5h + hebdo 7j) ----------------------
// Depuis le stdin uniquement, aucun appel réseau. Champs absents (ancienne
// version / juste après /clear) => segments masqués silencieusement.
const nowEpoch = Math.floor(Date.now() / 1000);
// Formate un delta de secondes en "XhYYm" (ou "YYm" si < 1h, "0m" si passé)
function fmtCountdown(d) {
if (!Number.isFinite(d) || d <= 0) return '0m';
const h = Math.floor(d / 3600);
const m = Math.floor((d % 3600) / 60);
return h > 0 ? `${h}h${String(m).padStart(2, '0')}m` : `${m}m`;
}
// Formate un timestamp epoch en "jour heure:min" (heure locale, comme date -r)
function fmtDatetime(ts) {
const d = new Date(Number(ts) * 1000);
if (isNaN(d.getTime())) return '';
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const hh = String(d.getHours()).padStart(2, '0');
const mm = String(d.getMinutes()).padStart(2, '0');
return `${days[d.getDay()]} ${hh}:${mm}`;
}
function pctColorFor(p) {
const pi = Math.round(Number(p));
if (pi >= 80) return RED;
if (pi >= 50) return YELLOW;
return GREEN;
}
// -- Session 5h --
const fiveHourPct = get(input, 'rate_limits.five_hour.used_percentage');
const fiveHourReset = get(input, 'rate_limits.five_hour.resets_at');
let fiveHourStr = '';
if (fiveHourPct != null) {
fiveHourStr = `⏳ ${pctColorFor(fiveHourPct)}${Math.round(Number(fiveHourPct))}%${RESET}`;
if (fiveHourReset != null) {
const left = fmtCountdown(Number(fiveHourReset) - nowEpoch);
fiveHourStr += ` ${GRAY}(${left})${RESET}`;
}
}
// -- Hebdo 7j --
const sevenDayPct = get(input, 'rate_limits.seven_day.used_percentage');
const sevenDayReset = get(input, 'rate_limits.seven_day.resets_at');
let sevenDayStr = '';
if (sevenDayPct != null) {
sevenDayStr = `📅 ${pctColorFor(sevenDayPct)}${Math.round(Number(sevenDayPct))}%${RESET}`;
if (sevenDayReset != null) {
const when = fmtDatetime(sevenDayReset);
if (when) sevenDayStr += ` ${GRAY}(${when})${RESET}`;
}
}
// --- 6. Organisation + login method ----------------------------------------
let claudeJson = '';
const cfgDir = process.env.CLAUDE_CONFIG_DIR;
if (cfgDir && fs.existsSync(path.join(cfgDir, '.claude.json'))) {
claudeJson = path.join(cfgDir, '.claude.json');
} else if (fs.existsSync(path.join(os.homedir(), '.claude.json'))) {
claudeJson = path.join(os.homedir(), '.claude.json');
}
let orgName = '';
let orgType = '';
if (claudeJson) {
const cj = readJson(claudeJson);
if (cj && cj.oauthAccount) {
orgName = cj.oauthAccount.organizationName || '';
orgType = cj.oauthAccount.organizationType || '';
}
}
let loginLabel;
if (process.env.CLAUDE_CODE_USE_BEDROCK) loginLabel = 'bedrock';
else if (process.env.CLAUDE_CODE_USE_VERTEX) loginLabel = 'vertex';
else if (process.env.CLAUDE_CODE_USE_FOUNDRY) loginLabel = 'foundry';
else if (process.env.ANTHROPIC_AUTH_TOKEN) loginLabel = 'gateway';
else if (process.env.ANTHROPIC_API_KEY) loginLabel = 'api-key';
else if (process.env.CLAUDE_CODE_OAUTH_TOKEN) loginLabel = 'oauth-token';
else if (orgName) loginLabel = 'oauth';
else loginLabel = '?';
// --- Assemble ---------------------------------------------------------------
const SEP = `${GRAY} | ${RESET}`;
let out = `${BOLD}${BLUE}${model}${RESET}`;
if (effortLabel) out += `${SEP}💪 ${effortColor}${effortLabel}${RESET}`;
if (msgCount > 0) out += `${SEP}${CYAN}💬 ${msgCount}${respCount}${RESET}`;
if (hasPct) {
out += `${SEP}🪟 ${barColor}${pctStr}${RESET}`;
if (tokenStr) out += ` ${GRAY}${tokenStr}${RESET}`;
}
// Rate limits Claude.ai (session 5h + hebdo 7j) — masqués si absents du stdin
if (fiveHourStr) out += `${SEP}${fiveHourStr}`;
if (sevenDayStr) out += `${SEP}${sevenDayStr}`;
// Git branch juste après les rate limits (avant le dossier)
if (gitBranch) out += `${SEP}${GREEN}🌿 ${gitBranch}${RESET}`;
if (currentDirName) out += `${SEP}${CYAN}📁 ${currentDirName}${RESET}`;
// Organisation puis méthode de login, en fin de barre
if (orgName) {
let orgDisp = orgName;
if (orgType) orgDisp += ` (${orgType})`;
out += `${SEP}${MAGENTA}🏢 ${orgDisp}${RESET}`;
}
out += `${SEP}${CYAN}🔑 ${loginLabel}${RESET}`;
process.stdout.write(out + '\n');
@enimiste

enimiste commented Jul 6, 2026

Copy link
Copy Markdown
Author

Two files :

  • One for Macosx and Linux : Requires jq lib to be present on the machine.
  • One for Windows : Requires nodejs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment