Development
Hook
Hooks
What claudecode-statusline runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Where it lives
- hooks/caveman-activate.jsGitHub
Read the script
#!/usr/bin/env node // caveman — Claude Code SessionStart activation hook // // Runs on every session start: // 1. Writes flag file at $CLAUDE_CONFIG_DIR/.caveman-active (statusline reads this) // 2. Emits caveman ruleset as hidden SessionStart context // 3. Detects missing statusline config and emits setup nudge const fs = require('fs'); const path = require('path'); const os = require('os'); const { getDefaultMode, safeWriteFlag } = require('./caveman-config'); const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude'); const flagPath = path.join(claudeDir, '.caveman-active'); const settingsPath = path.join(claudeDir, 'settings.json'); const mode = getDefaultMode(); // "off" mode — skip activation entirely, don't write flag or emit rules if (mode === 'off') { try { fs.unlinkSync(flagPath); } catch (e) {} process.stdout.write('OK'); process.exit(0); } // 1. Write flag file (symlink-safe) safeWriteFlag(flagPath, mode); // 2. Emit full caveman ruleset, filtered to the active intensity level. // The old 2-sentence summary was too weak — models drifted back to verbose // mid-conversation, especially after context compression pruned it away. // Full rules with examples anchor behavior much more reliably. // // Reads SKILL.md at runtime so edits to the source of truth propagate // automatically — no hardcoded duplication to go stale. // Modes that have their own independent skill files — not caveman intensity levels. // For these, emit a short activation line; the skill itself handles behavior. const INDEPENDENT_MODES = new Set(['commit', 'review', 'compress']); if (INDEPENDENT_MODES.has(mode)) { process.stdout.write('CAVEMAN MODE ACTIVE — level: ' + mode + '. Behavior defined by /caveman-' + mode + ' skill.'); process.exit(0); } // Resolve the canonical label for wenyan alias const modeLabel = mode === 'wenyan' ? 'wenyan-full' : mode; // Read SKILL.md — the single source of truth for caveman behavior. // Plugin installs: __dirname = <plugin_root>/hooks/, SKILL.md at <plugin_root>/skills/caveman/SKILL.md // Standalone installs: __dirname = $CLAUDE_CONFIG_DIR/hooks/, SKILL.md won't exist — falls back to hardcoded rules. let skillContent = ''; try { skillContent = fs.readFileSync( path.join(__dirname, '..', 'skills', 'caveman', 'SKILL.md'), 'utf8' ); } catch (e) { /* standalone install — will use fallback below */ } let output; if (skillContent) { // Strip YAML frontmatter const body = skillContent.replace(/^---[\s\S]*?---\s*/, ''); // Filter intensity table: keep header rows + only the active level's row const filtered = body.split('\n').reduce((acc, line) => { // Intensity table rows start with | **level** | const tableRowMatch = line.match(/^\|\s*\*\*(\S+?)\*\*\s*\|/); if (tableRowMatch) { // Keep only the active level's row (and always keep header/separator) if (tableRowMatch[1] === modeLabel) { acc.push(line); } return acc; } // Example lines start with "- level:" — keep only lines matching active level const exampleMatch = line.match(/^- (\S+?):\s/); if (exampleMatch) { if (exampleMatch[1] === modeLabel) { acc.push(line); } return acc; } acc.push(line); return acc; }, []); output = 'CAVEMAN MODE ACTIVE — level: ' + modeLabel + '\n\n' + filtered.join('\n'); } else { // Fallback when SKILL.md is not found (standalone hook install without skills dir). // This is the minimum viable ruleset — better than nothing. output = 'CAVEMAN MODE ACTIVE — level: ' + modeLabel + '\n\n' + 'Respond terse like smart caveman. All technical substance stay. Only fluff die.\n\n' + '## Persistence\n\n' + 'ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode".\n\n' + 'Current level: **' + modeLabel + '**. Switch: `/caveman lite|full|ultra`.\n\n' + '## Rules\n\n' + 'Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. ' + 'Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact.\n\n' + 'Pattern: `[thing] [action] [reason]. [next step].`\n\n' + 'Not: "Sure! I\'d be happy to help you with that. The issue you\'re experiencing is likely caused by..."\n' + 'Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"\n\n' + '## Auto-Clarity\n\n' + 'Drop caveman for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user asks to clarify or repeats question. Resume caveman after clear part done.\n\n' + '## Boundaries\n\n' + 'Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end.'; } // 3. Detect missing statusline config — nudge Claude to help set it up try { let hasStatusline = false; if (fs.existsSync(settingsPath)) { const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); if (settings.statusLine) { hasStatusline = true; } } if (!hasStatusline) { const isWindows = process.platform === 'win32'; const scriptName = isWindows ? 'caveman-statusline.ps1' : 'caveman-statusline.sh'; const scriptPath = path.join(__dirname, scriptName); const command = isWindows ? `powershell -ExecutionPolicy Bypass -File "${scriptPath}"` : `bash "${scriptPath}"`; const statusLineSnippet = '"statusLine": { "type": "command", "command": ' + JSON.stringify(command) + ' }'; output += "\n\n" + "STATUSLINE SETUP NEEDED: The caveman plugin includes a statusline badge showing active mode " + "(e.g. [CAVEMAN], [CAVEMAN:ULTRA]). It is not configured yet. " + "To enable, add this to " + path.join(claudeDir, 'settings.json - hooks/caveman-config.jsGitHub
Read the script
#!/usr/bin/env node // caveman — shared configuration resolver // // Resolution order for default mode: // 1. CAVEMAN_DEFAULT_MODE environment variable // 2. Config file defaultMode field: // - $XDG_CONFIG_HOME/caveman/config.json (any platform, if set) // - ~/.config/caveman/config.json (macOS / Linux fallback) // - %APPDATA%\caveman\config.json (Windows fallback) // 3. 'full' const fs = require('fs'); const path = require('path'); const os = require('os'); const VALID_MODES = [ 'off', 'lite', 'full', 'ultra', 'wenyan-lite', 'wenyan', 'wenyan-full', 'wenyan-ultra', 'commit', 'review', 'compress' ]; function getConfigDir() { if (process.env.XDG_CONFIG_HOME) { return path.join(process.env.XDG_CONFIG_HOME, 'caveman'); } if (process.platform === 'win32') { return path.join( process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'caveman' ); } return path.join(os.homedir(), '.config', 'caveman'); } function getConfigPath() { return path.join(getConfigDir(), 'config.json'); } function getDefaultMode() { // 1. Environment variable (highest priority) const envMode = process.env.CAVEMAN_DEFAULT_MODE; if (envMode && VALID_MODES.includes(envMode.toLowerCase())) { return envMode.toLowerCase(); } // 2. Config file try { const configPath = getConfigPath(); const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); if (config.defaultMode && VALID_MODES.includes(config.defaultMode.toLowerCase())) { return config.defaultMode.toLowerCase(); } } catch (e) { // Config file doesn't exist or is invalid — fall through } // 3. Default return 'full'; } // Symlink-safe flag file write. // Uses O_NOFOLLOW where available, writes atomically via temp + rename with // 0600 permissions. Protects against local attackers replacing the predictable // flag path (~/.claude/.caveman-active) with a symlink to clobber other files. // // When the parent directory is itself a symlink (legitimate pattern: ~/.claude // symlinked to another drive or shared config dir), resolves through to the // real path and verifies ownership on Unix (uid match). This allows e.g. // ln -s /opt/shared-claude-config ~/.claude // while still refusing attacker-planted symlinks pointing to dirs owned by // another user. // // On Windows, uid checks are unavailable — falls back to verifying the resolved // path lives under the user's home directory. // // The flag file itself must never be a symlink (that's the actual clobber vector). // // Set CAVEMAN_DEBUG=1 to emit stderr diagnostics when flag writes are refused. // // Silent-fails on any filesystem error — the flag is best-effort. function safeWriteFlag(flagPath, content) { const debug = process.env.CAVEMAN_DEBUG === '1'; try { const flagDir = path.dirname(flagPath); fs.mkdirSync(flagDir, { recursive: true }); // When the parent directory is a symlink, resolve it and verify ownership. // This allows legitimate symlinked ~/.claude dirs while still refusing // attacker-planted symlinks pointing at dirs owned by another user. let realFlagDir; try { const lstat = fs.lstatSync(flagDir); if (lstat.isSymbolicLink()) { realFlagDir = fs.realpathSync(flagDir); const realStat = fs.statSync(realFlagDir); if (!realStat.isDirectory()) { if (debug) process.stderr.write(`[caveman] safeWriteFlag: symlink target ${realFlagDir} is not a directory\n`); return; } if (typeof process.getuid === 'function') { if (realStat.uid !== process.getuid()) { if (debug) process.stderr.write(`[caveman] safeWriteFlag: symlink target ${realFlagDir} owned by uid ${realStat.uid}, not current user ${process.getuid()}\n`); return; } } else { const home = os.homedir(); const normalizedReal = path.resolve(realFlagDir); const normalizedHome = path.resolve(home); if (!normalizedReal.toLowerCase().startsWith(normalizedHome.toLowerCase() + path.sep) && normalizedReal.toLowerCase() !== normalizedHome.toLowerCase()) { if (debug) process.stderr.write(`[caveman] safeWriteFlag: symlink target ${normalizedReal} is outside home directory ${normalizedHome}\n`); return; } } } else { realFlagDir = flagDir; } } catch (e) { return; } // The flag file itself must never be a symlink (that's the actual clobber vector). const realFlagPath = path.join(realFlagDir, path.basename(flagPath)); try { if (fs.lstatSync(realFlagPath).isSymbolicLink()) return; } catch (e) { if (e.code !== 'ENOENT') return; } const tempPath = path.join(realFlagDir, `.caveman-active.${process.pid}.${Date.now()}`); const O_NOFOLLOW = typeof fs.constants.O_NOFOLLOW === 'number' ? fs.constants.O_NOFOLLOW : 0; const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW; let fd; try { fd = fs.openSync(tempPath, flags, 0o600); fs.writeSync(fd, String(content)); try { fs.fchmodSync(fd, 0o600); } catch (e) { /* best-effort on Windows */ } } finally { if (fd !== undefined) fs.closeSync(fd); } fs.renameSync(tempPath, realFlagPath); } catch (e) { // Silent fail — flag is best-effort } } // Symlink-safe, size-capped, whitelist-validated flag file read. // Symmetric with safeWriteFlag: refuses symlinks at the target, caps the read, // and rejects anything that isn't a known mode. Returns null on any anomaly. // // Without this, a local attacker with write access to ~/.claude/ could replace // the flag with a symlink to ~/.ssh/id_rsa (or any user-readable secret). Every // reader — statusline, per-turn reinforcement — would slurp that content and // either echo it to the terminal or inject it into model context. // // MAX_FLAG_BYTES is a hard cap. - hooks/caveman-mode-tracker.jsGitHub
Read the script
#!/usr/bin/env node // caveman — UserPromptSubmit hook to track which caveman mode is active // Inspects user input for /caveman commands and writes mode to flag file const fs = require('fs'); const path = require('path'); const os = require('os'); const { execFileSync } = require('child_process'); const { getDefaultMode, safeWriteFlag, readFlag, VALID_MODES } = require('./caveman-config'); // Modes handled by their own slash commands (/caveman-commit, etc.) — not // selectable via /caveman <arg>. const INDEPENDENT_MODES = new Set(['commit', 'review', 'compress']); const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude'); const flagPath = path.join(claudeDir, '.caveman-active'); let input = ''; process.stdin.on('data', chunk => { input += chunk; }); process.stdin.on('end', () => { try { const data = JSON.parse(input); const prompt = (data.prompt || '').trim().toLowerCase(); // Natural language activation (e.g. "activate caveman", "turn on caveman mode", // "talk like caveman"). README tells users they can say these, but the hook // only matched /caveman commands — flag file and statusline stayed out of sync. if (/\b(activate|enable|turn on|start|talk like)\b.*\bcaveman\b/i.test(prompt) || /\bcaveman\b.*\b(mode|activate|enable|turn on|start)\b/i.test(prompt)) { if (!/\b(stop|disable|turn off|deactivate)\b/i.test(prompt)) { const mode = getDefaultMode(); if (mode !== 'off') { safeWriteFlag(flagPath, mode); } } } // /caveman-stats [--share] — block the prompt and inject stats output as // the hook's reason. The script reads the active session log, so we pass // transcript_path through when Claude Code provides it. const statsMatch = /^\/caveman(?::caveman)?-stats(?:\s+(.*))?$/.exec(prompt); if (statsMatch) { const tailArgs = (statsMatch[1] || '').trim().split(/\s+/).filter(Boolean); try { const statsPath = path.join(__dirname, 'caveman-stats.js'); const argv = [statsPath]; if (data.transcript_path) argv.push('--session-file', data.transcript_path); if (tailArgs.includes('--share')) argv.push('--share'); if (tailArgs.includes('--all')) argv.push('--all'); const sinceIdx = tailArgs.indexOf('--since'); if (sinceIdx !== -1 && tailArgs[sinceIdx + 1]) { argv.push('--since', tailArgs[sinceIdx + 1]); } const out = execFileSync(process.execPath, argv, { encoding: 'utf8', timeout: 5000 }); process.stdout.write(JSON.stringify({ decision: 'block', reason: out.trim() })); } catch (e) { process.stdout.write(JSON.stringify({ decision: 'block', reason: 'caveman-stats: could not run stats script.\nTry manually: node hooks/caveman-stats.js' })); } return; } // Match /caveman commands if (prompt.startsWith('/caveman')) { const parts = prompt.split(/\s+/); const cmd = parts[0]; // /caveman, /caveman-commit, /caveman-review, etc. const arg = parts[1] || ''; let mode = null; if (cmd === '/caveman-commit') { mode = 'commit'; } else if (cmd === '/caveman-review') { mode = 'review'; } else if (cmd === '/caveman-compress' || cmd === '/caveman:caveman-compress') { mode = 'compress'; } else if (cmd === '/caveman' || cmd === '/caveman:caveman') { // Bare /caveman → activate at configured default if (!arg) { mode = getDefaultMode(); } else if (arg === 'off' || arg === 'stop' || arg === 'disable') { mode = 'off'; } else if (arg === 'wenyan-full') { // Canonical alias — config stores as 'wenyan' mode = 'wenyan'; } else if (VALID_MODES.includes(arg) && !INDEPENDENT_MODES.has(arg)) { mode = arg; } // Unknown arg → mode stays null, flag untouched (no silent overwrite) } if (mode && mode !== 'off') { safeWriteFlag(flagPath, mode); } else if (mode === 'off') { try { fs.unlinkSync(flagPath); } catch (e) {} } } // Detect deactivation — natural language and slash commands if (/\b(stop|disable|deactivate|turn off)\b.*\bcaveman\b/i.test(prompt) || /\bcaveman\b.*\b(stop|disable|deactivate|turn off)\b/i.test(prompt) || /\bnormal mode\b/i.test(prompt)) { try { fs.unlinkSync(flagPath); } catch (e) {} } // Per-turn reinforcement: emit a structured reminder when caveman is active. // The SessionStart hook injects the full ruleset once, but models lose it // when other plugins inject competing style instructions every turn. // This keeps caveman visible in the model's attention on every user message. // // Skip independent modes (commit, review, compress) — they have their own // skill behavior and the base caveman rules would conflict. // readFlag enforces symlink-safe read + size cap + VALID_MODES whitelist. // If the flag is missing, corrupted, oversized, or a symlink pointing at // something like ~/.ssh/id_rsa, readFlag returns null and we emit nothing // — never inject untrusted bytes into model context. const activeMode = readFlag(flagPath); if (activeMode && !INDEPENDENT_MODES.has(activeMode)) { process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: "CAVEMAN MODE ACTIVE (" + activeMode + "). " + "Drop articles/filler/pleasantries/hedging. Fragments OK. " + "Code/commits/security: write normal." } })); } } catch (e) { // Silent fail } }); - hooks/caveman-stats.jsGitHub
Read the script
#!/usr/bin/env node // caveman-stats — read the active Claude Code session log, print real token // usage plus an estimated savings figure from the benchmark in benchmarks/. // // Run directly: node hooks/caveman-stats.js // Inside Claude: /caveman-stats triggers this via the UserPromptSubmit hook. // Hook integration passes --session-file <transcript_path> so we always read // the active session, not whichever JSONL was modified most recently. const fs = require('fs'); const path = require('path'); const os = require('os'); const { readFlag, appendFlag, readHistory, safeWriteFlag } = require('./caveman-config'); // Mean per-task savings from benchmarks/results/*.json (avg_savings: 65 across // 10 tasks, sonnet-4-20250514). Only 'full' has measured data; lite / ultra / // wenyan modes show no estimate until benchmarked. Add an entry here when a new // run is committed. const COMPRESSION = { 'full': 0.65 }; // Approximate Anthropic public output-token pricing, USD per million. // Match by model id prefix so this stays correct across point releases // (e.g. claude-sonnet-4-20250514, claude-sonnet-4-7). Update from // https://www.anthropic.com/pricing if a release changes the tier. const MODEL_OUTPUT_PRICE_PER_M = [ ['claude-opus-4', 75.00], ['claude-sonnet-4', 15.00], ['claude-haiku-4', 4.00], ['claude-3-5-sonnet', 15.00], ['claude-3-5-haiku', 4.00], ['claude-3-opus', 75.00], ]; function priceForModel(model) { if (!model) return null; for (const [prefix, price] of MODEL_OUTPUT_PRICE_PER_M) { if (model.startsWith(prefix)) return price; } return null; } function formatUsd(amount) { if (amount >= 1) return `$${amount.toFixed(2)}`; if (amount >= 0.01) return `$${amount.toFixed(3)}`; return `$${amount.toFixed(4)}`; } function findRecentSession(claudeDir) { const projectsDir = path.join(claudeDir, 'projects'); let entries; try { entries = fs.readdirSync(projectsDir, { withFileTypes: true }); } catch { return null; } let best = null; const stack = entries.map(e => path.join(projectsDir, e.name)); while (stack.length) { const p = stack.pop(); let st; try { st = fs.statSync(p); } catch { continue; } if (st.isDirectory()) { try { for (const child of fs.readdirSync(p)) stack.push(path.join(p, child)); } catch {} } else if (p.endsWith('.jsonl') && (!best || st.mtimeMs > best.mtime)) { best = { file: p, mtime: st.mtimeMs }; } } return best ? best.file : null; } function parseSession(filePath) { let raw; try { raw = fs.readFileSync(filePath, 'utf8'); } catch { return { outputTokens: 0, cacheReadTokens: 0, turns: 0, model: null }; } let outputTokens = 0; let cacheReadTokens = 0; let turns = 0; let model = null; for (const line of raw.split('\n')) { if (!line.trim()) continue; let entry; try { entry = JSON.parse(line); } catch { continue; } if (entry.type !== 'assistant' || !entry.message) continue; const usage = entry.message.usage; if (!usage) continue; outputTokens += usage.output_tokens || 0; cacheReadTokens += usage.cache_read_input_tokens || 0; turns++; if (!model && entry.message.model) model = entry.message.model; } return { outputTokens, cacheReadTokens, turns, model }; } // Detect *.original.md / *.md pairs left behind by caveman-compress. The // presence of a *.original.md backup means the *.md sibling is a compressed // memory file — every session start reads the compressed version, so the // delta is per-session input-token savings (passive). Returns a summary or // null if nothing was found in the given dirs. function findCompressedPairs(dirs) { const pairs = []; for (const dir of dirs) { let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; } for (const entry of entries) { if (!entry.isFile() || !entry.name.endsWith('.original.md')) continue; const base = entry.name.slice(0, -'.original.md'.length); const originalPath = path.join(dir, entry.name); const compressedPath = path.join(dir, `${base}.md`); let oSize, cSize; try { oSize = fs.statSync(originalPath).size; cSize = fs.statSync(compressedPath).size; } catch { continue; } if (oSize <= cSize) continue; pairs.push({ name: base, dir, originalSize: oSize, compressedSize: cSize }); } } return pairs; } function summarizeCompressed(pairs) { if (!pairs || pairs.length === 0) return null; const totalOriginal = pairs.reduce((s, p) => s + p.originalSize, 0); const totalCompressed = pairs.reduce((s, p) => s + p.compressedSize, 0); const bytesSaved = totalOriginal - totalCompressed; // English prose runs ~4 chars per token. Label result as approximate so we // don't make claims tighter than the method warrants. const tokensSaved = Math.round(bytesSaved / 4); return { count: pairs.length, bytesSaved, tokensSaved }; } // Compute the savings figures we want to log/share for one session snapshot. function deriveSavings({ outputTokens, mode, model }) { const ratio = COMPRESSION[mode] != null ? COMPRESSION[mode] : null; const price = priceForModel(model); if (ratio === null) return { estSavedTokens: 0, estSavedUsd: 0 }; const estNormal = Math.round(outputTokens / (1 - ratio)); const estSavedTokens = estNormal - outputTokens; const estSavedUsd = price !== null ? (estSavedTokens / 1_000_000) * price : 0; return { estSavedTokens, estSavedUsd }; } // Parse "7d", "12h" etc. to milliseconds. Returns null on invalid input. function parseDuration(spec) { if (!spec) return null; const m = /^(\d+)([dh])$/.exec(spec.trim()); if (!m) return null; const n = parseInt(m[1], 10); return m[2] === 'd' ? n * 86_400_000 : n * 3_600_000; } // Aggregate history into latest-per-session totals, optionally filtered to a // time window. Returns { sessions, outputTokens, estSavedTokens, estSavedUsd }. fun - hooks/caveman-statusline.shGitHub
Read the script
#!/bin/bash # claudecode-statusline — Claude Code statusline renderer # Shows: caveman mode, project/folder, git branch, model, ctx%, effort, # 5h and 7d rate-limit bars with used%/rem% + time-to-reset. # # Component env vars (set to 0 to hide): # SL_CAVEMAN=0 SL_PROJECT=0 SL_MODEL=0 SL_CTX=0 # SL_EFFORT=0 SL_5H=0 SL_7D=0 SL_SAVINGS=0 # ── helpers ─────────────────────────────────────────────────────────────────── fill_bar() { local pct="${1:-0}" width="${2:-10}" local filled=$(( pct * width / 100 )) [ $filled -gt $width ] && filled=$width local empty=$(( width - filled )) local bar="" i=0 while [ $i -lt $filled ]; do bar="${bar}█"; i=$(( i + 1 )); done while [ $i -lt $width ]; do bar="${bar}░"; i=$(( i + 1 )); done printf '%s' "$bar" } fmt_seconds() { local secs="${1:-0}" local now remaining now=$(date +%s) remaining=$(( secs - now )) [ $remaining -lt 0 ] && remaining=0 local days=$(( remaining / 86400 )) local hrs=$(( (remaining % 86400) / 3600 )) local mins=$(( (remaining % 3600) / 60 )) if [ $days -gt 0 ]; then printf '%dd %dh' "$days" "$hrs" elif [ $hrs -gt 0 ]; then printf '%dh %dm' "$hrs" "$mins" else printf '%dm' "$mins" fi } # Return ANSI color for a "used" percentage (green→yellow→red as usage rises) used_color() { local pct="${1:-0}" if [ "$pct" -ge 80 ]; then printf '\033[38;5;196m' # red elif [ "$pct" -ge 50 ]; then printf '\033[38;5;220m' # yellow else printf '\033[38;5;114m' # green fi } # Return ANSI color for a "remaining" percentage (red→yellow→green as remainder rises) rem_color() { local pct="${1:-0}" if [ "$pct" -le 20 ]; then printf '\033[38;5;196m' # red — almost none left elif [ "$pct" -le 50 ]; then printf '\033[38;5;220m' # yellow else printf '\033[38;5;114m' # green — plenty left fi } # ── read stdin JSON ─────────────────────────────────────────────────────────── INPUT=$(cat) # ── caveman badge ───────────────────────────────────────────────────────────── CAVEMAN_BADGE="" if [ "${SL_CAVEMAN:-1}" != "0" ]; then FLAG="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.caveman-active" if [ ! -L "$FLAG" ] && [ -f "$FLAG" ]; then MODE=$(head -c 64 "$FLAG" 2>/dev/null | tr -d '\n\r' | tr '[:upper:]' '[:lower:]') MODE=$(printf '%s' "$MODE" | tr -cd 'a-z0-9-') case "$MODE" in off|lite|full|ultra|wenyan-lite|wenyan|wenyan-full|wenyan-ultra|commit|review|compress) if [ -z "$MODE" ] || [ "$MODE" = "full" ]; then CAVEMAN_BADGE=$(printf '\033[38;5;172m[🪨 CAVEMAN]\033[0m') else SUFFIX=$(printf '%s' "$MODE" | tr '[:lower:]' '[:upper:]') CAVEMAN_BADGE=$(printf '\033[38;5;172m[🪨 CAVEMAN:%s]\033[0m' "$SUFFIX") fi ;; esac fi fi # ── project / folder name ───────────────────────────────────────────────────── PROJECT_BADGE="" GIT_BRANCH="" if [ "${SL_PROJECT:-1}" != "0" ]; then PROJECT_DIR=$(printf '%s' "$INPUT" | jq -r '.workspace.project_dir // .workspace.current_dir // .cwd // empty' 2>/dev/null) [ -z "$PROJECT_DIR" ] && PROJECT_DIR=$(pwd) PROJECT_NAME=$(basename "$PROJECT_DIR") PROJECT_BADGE=$(printf '\033[38;5;75m📁 %s\033[0m' "$PROJECT_NAME") if command -v git >/dev/null 2>&1; then GIT_DIR_ARG="" [ -n "$PROJECT_DIR" ] && [ -d "$PROJECT_DIR/.git" ] && GIT_DIR_ARG="-C $PROJECT_DIR" BRANCH=$(git $GIT_DIR_ARG -c core.hooksPath=/dev/null branch --show-current 2>/dev/null) [ -n "$BRANCH" ] && GIT_BRANCH=$(printf '\033[38;5;114m 🌿 %s\033[0m' "$BRANCH") fi fi # ── model ───────────────────────────────────────────────────────────────────── MODEL_BADGE="" if [ "${SL_MODEL:-1}" != "0" ]; then MODEL=$(printf '%s' "$INPUT" | jq -r '.model.display_name // empty' 2>/dev/null) [ -n "$MODEL" ] && MODEL_BADGE=$(printf '\033[38;5;183m🤖 %s\033[0m' "$MODEL") fi # ── context window ──────────────────────────────────────────────────────────── CTX_BADGE="" if [ "${SL_CTX:-1}" != "0" ]; then USED_PCT=$(printf '%s' "$INPUT" | jq -r '.context_window.used_percentage // empty' 2>/dev/null) if [ -n "$USED_PCT" ] && [ "$USED_PCT" != "null" ]; then USED_INT=$(printf '%.0f' "$USED_PCT" 2>/dev/null || echo 0) CTX_BAR=$(fill_bar "$USED_INT" 8) BAR_COLOR=$(used_color "$USED_INT") CTX_BADGE=$(printf "${BAR_COLOR}CTX %s %d%%\033[0m" "$CTX_BAR" "$USED_INT") fi fi # ── effort level ────────────────────────────────────────────────────────────── EFFORT_BADGE="" if [ "${SL_EFFORT:-1}" != "0" ]; then EFFORT=$(printf '%s' "$INPUT" | jq -r '.effort.level // empty' 2>/dev/null) if [ -n "$EFFORT" ] && [ "$EFFORT" != "null" ]; then EFFORT_UP=$(printf '%s' "$EFFORT" | tr '[:lower:]' '[:upper:]') case "$EFFORT" in low) EFFORT_COLOR='\033[38;5;244m'; EFFORT_ICON="▁" ;; medium) EFFORT_COLOR='\033[38;5;75m'; EFFORT_ICON="▄" ;; high) EFFORT_COLOR='\033[38;5;220m'; EFFORT_ICON="▇" ;; xhigh) EFFORT_COLOR='\033[38;5;208m'; EFFORT_ICON="█" ;; max) EFFORT_COLOR='\033[38;5;196m'; EFFORT_ICON="█" ;; *) EFFORT_COLOR='\033[38;5;244m'; EFFORT_ICON="?" ;; esac EFFORT_BADGE=$(printf "${EFFORT_COLOR}⚡ %s %s\033[0m" "$EFFORT_ICON" "$EFFORT_UP") fi fi # ── 5-hour rate limit ───────────────────────────────────────────────────────── FIVE_H_BADGE="" if [ "${SL_5H:-1}" != "0" ]; then FIVE_H_PCT=$(printf '%s' "$INPUT" | jq -r '.rate_limits.five_hour.used_percentage // empty' 2>/dev/null) FIVE_H_RESET=$(printf '%s' "$INPUT" | jq -r '.rate_limits.five_hour.resets_at // empty' 2>/dev/null) if [ -n "$FIVE_H_PCT" ] && [ "$FIVE_H_PCT" != "null" ]; then FIVE_H_INT=$(printf '%.0f' "$FIVE_H_PCT" 2>/dev/null || echo 0) FIVE_H_REM=$(( 100 - FIVE_H_INT )) FIVE_H_BAR=$(fill_bar "$FIVE_H_INT" 8) U_COLOR=$(used_color "$FIVE_H_INT") R_COLOR=$(rem_color "$FIVE_H_REM") RESET_STR="" if [ -n "$FIVE_H_RESET" ] && [ "$FIVE_H_RESET" != "null" ] && [ "$FIV
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withclaudecode-statusline
A one-command setup for a richly decorated Claude Code statusline with Caveman mode baked in. Interactive installer lets you pick all components or only the ones you want.
Get the whole plugin
Stats
15
Stars
1
Forks
Maintained
Maintenance
JavaScript
Language
MIT
License
4mo ago
Last commit
4mo ago
Created
Repo: FahimFBA/claudecode-statusline

