Created
February 15, 2026 04:10
-
-
Save rockfordlhotka/6b6360c79efad41f9480659faf8bd2db to your computer and use it in GitHub Desktop.
Claude Code status line — shows model, active task, git branch/status, context usage bar, 5h/7d plan usage, cost, duration, and lines changed. Setup: save as ~/.claude/statusline.js, then add {"statusLine":{"type":"command","command":"node ~/.claude/statusline.js"}} to ~/.claude/settings.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| // Claude Code Statusline — inspired by GSD (get-shit-done) | |
| // Shows: model | task | git | context bar | usage (5h/7d) | cost | duration | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const os = require('os'); | |
| const https = require('https'); | |
| const { execSync } = require('child_process'); | |
| let input = ''; | |
| process.stdin.setEncoding('utf8'); | |
| process.stdin.on('data', chunk => input += chunk); | |
| process.stdin.on('end', () => { | |
| try { | |
| const data = JSON.parse(input); | |
| const model = data.model?.display_name || 'Claude'; | |
| const dir = data.workspace?.current_dir || process.cwd(); | |
| const session = data.session_id || ''; | |
| const remaining = data.context_window?.remaining_percentage; | |
| const cost = data.cost?.total_cost_usd || 0; | |
| const durationMs = data.cost?.total_duration_ms || 0; | |
| const linesAdded = data.cost?.total_lines_added || 0; | |
| const linesRemoved = data.cost?.total_lines_removed || 0; | |
| // ANSI helpers | |
| const DIM = '\x1b[2m'; | |
| const BOLD = '\x1b[1m'; | |
| const GREEN = '\x1b[32m'; | |
| const YELLOW = '\x1b[33m'; | |
| const RED = '\x1b[31m'; | |
| const CYAN = '\x1b[36m'; | |
| const ORANGE = '\x1b[38;5;208m'; | |
| const BLINK_RED = '\x1b[5;31m'; | |
| const RESET = '\x1b[0m'; | |
| const SEP = ` ${DIM}\u2502${RESET} `; | |
| // --- Plan usage (cached, async-safe via file) --- | |
| let usageSection = ''; | |
| const usageCacheFile = path.join(os.tmpdir(), 'claude-statusline-usage-cache.json'); | |
| const USAGE_CACHE_TTL = 60000; // 60 seconds | |
| let usageCached = null; | |
| try { | |
| if (fs.existsSync(usageCacheFile)) { | |
| usageCached = JSON.parse(fs.readFileSync(usageCacheFile, 'utf8')); | |
| if (Date.now() - usageCached.ts > USAGE_CACHE_TTL) usageCached = null; | |
| } | |
| } catch (e) { usageCached = null; } | |
| // Fire-and-forget refresh when cache is stale | |
| if (!usageCached) { | |
| try { | |
| const credsPath = path.join(os.homedir(), '.claude', '.credentials.json'); | |
| if (fs.existsSync(credsPath)) { | |
| const creds = JSON.parse(fs.readFileSync(credsPath, 'utf8')); | |
| const token = creds.claudeAiOauth?.accessToken; | |
| if (token) { | |
| const req = https.request('https://api.anthropic.com/api/oauth/usage', { | |
| method: 'GET', | |
| headers: { | |
| 'Authorization': `Bearer ${token}`, | |
| 'User-Agent': 'claude-code/2.0.31', | |
| 'anthropic-beta': 'oauth-2025-04-20', | |
| }, | |
| timeout: 3000, | |
| }, (res) => { | |
| let body = ''; | |
| res.on('data', (d) => body += d); | |
| res.on('end', () => { | |
| try { | |
| const usage = JSON.parse(body); | |
| const cache = { | |
| ts: Date.now(), | |
| fiveHour: usage.five_hour?.utilization ?? null, | |
| fiveHourResets: usage.five_hour?.resets_at ?? null, | |
| sevenDay: usage.seven_day?.utilization ?? null, | |
| sevenDayResets: usage.seven_day?.resets_at ?? null, | |
| }; | |
| fs.writeFileSync(usageCacheFile, JSON.stringify(cache)); | |
| } catch (e) { /* silent */ } | |
| }); | |
| }); | |
| req.on('error', () => {}); | |
| req.on('timeout', () => req.destroy()); | |
| req.end(); | |
| } | |
| } | |
| } catch (e) { /* silent */ } | |
| } | |
| // Render usage from cache (will show on next render if just refreshed) | |
| if (usageCached) { | |
| const colorForPct = (pct) => { | |
| if (pct < 50) return GREEN; | |
| if (pct < 75) return YELLOW; | |
| if (pct < 90) return ORANGE; | |
| return BLINK_RED; | |
| }; | |
| const parts = []; | |
| if (usageCached.fiveHour != null) { | |
| const pct = Math.round(usageCached.fiveHour); | |
| let resetStr = ''; | |
| if (usageCached.fiveHourResets) { | |
| const resetMs = new Date(usageCached.fiveHourResets).getTime() - Date.now(); | |
| if (resetMs > 0) { | |
| const h = Math.floor(resetMs / 3600000); | |
| const m = Math.floor((resetMs % 3600000) / 60000); | |
| resetStr = ` ${DIM}${h}h${m.toString().padStart(2, '0')}m${RESET}`; | |
| } | |
| } | |
| parts.push(`${colorForPct(pct)}5h:${pct}%${RESET}${resetStr}`); | |
| } | |
| if (usageCached.sevenDay != null) { | |
| const pct = Math.round(usageCached.sevenDay); | |
| parts.push(`${colorForPct(pct)}7d:${pct}%${RESET}`); | |
| } | |
| if (parts.length > 0) usageSection = parts.join(' '); | |
| } | |
| // --- Context bar (GSD-style: scale to 80% real limit) --- | |
| let ctxSection = ''; | |
| if (remaining != null) { | |
| const rawUsed = Math.max(0, Math.min(100, 100 - Math.round(remaining))); | |
| const used = Math.min(100, Math.round((rawUsed / 80) * 100)); | |
| const filled = Math.floor(used / 10); | |
| const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(10 - filled); | |
| let color; | |
| if (used < 63) color = GREEN; | |
| else if (used < 81) color = YELLOW; | |
| else if (used < 95) color = ORANGE; | |
| else color = BLINK_RED; | |
| ctxSection = `${color}${bar} ${used}%${RESET}`; | |
| } | |
| // --- Current task from todos (GSD pattern) --- | |
| let taskSection = ''; | |
| const todosDir = path.join(os.homedir(), '.claude', 'todos'); | |
| if (session && fs.existsSync(todosDir)) { | |
| try { | |
| const files = fs.readdirSync(todosDir) | |
| .filter(f => f.startsWith(session) && f.endsWith('.json')) | |
| .map(f => ({ name: f, mtime: fs.statSync(path.join(todosDir, f)).mtime })) | |
| .sort((a, b) => b.mtime - a.mtime); | |
| if (files.length > 0) { | |
| const todos = JSON.parse(fs.readFileSync(path.join(todosDir, files[0].name), 'utf8')); | |
| const inProgress = todos.find(t => t.status === 'in_progress'); | |
| if (inProgress) { | |
| taskSection = `${BOLD}${inProgress.activeForm || inProgress.subject || ''}${RESET}`; | |
| } | |
| } | |
| } catch (e) { /* silent */ } | |
| } | |
| // --- Git info (cached to avoid slowness) --- | |
| let gitSection = ''; | |
| const cacheFile = path.join(os.tmpdir(), 'claude-statusline-git-cache.json'); | |
| const CACHE_TTL = 5000; // 5 seconds | |
| let cached = null; | |
| try { | |
| if (fs.existsSync(cacheFile)) { | |
| cached = JSON.parse(fs.readFileSync(cacheFile, 'utf8')); | |
| if (Date.now() - cached.ts > CACHE_TTL || cached.dir !== dir) cached = null; | |
| } | |
| } catch (e) { cached = null; } | |
| if (!cached) { | |
| try { | |
| execSync('git rev-parse --git-dir', { cwd: dir, stdio: 'ignore' }); | |
| const branch = execSync('git branch --show-current', { cwd: dir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim(); | |
| const staged = execSync('git diff --cached --name-only', { cwd: dir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim(); | |
| const modified = execSync('git diff --name-only', { cwd: dir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim(); | |
| const stagedCount = staged ? staged.split('\n').length : 0; | |
| const modifiedCount = modified ? modified.split('\n').length : 0; | |
| cached = { ts: Date.now(), dir, branch, stagedCount, modifiedCount }; | |
| fs.writeFileSync(cacheFile, JSON.stringify(cached)); | |
| } catch (e) { | |
| cached = { ts: Date.now(), dir, branch: '', stagedCount: 0, modifiedCount: 0 }; | |
| fs.writeFileSync(cacheFile, JSON.stringify(cached)); | |
| } | |
| } | |
| if (cached.branch) { | |
| let indicators = ''; | |
| if (cached.stagedCount > 0) indicators += ` ${GREEN}+${cached.stagedCount}${RESET}`; | |
| if (cached.modifiedCount > 0) indicators += ` ${YELLOW}~${cached.modifiedCount}${RESET}`; | |
| gitSection = `${CYAN}${cached.branch}${RESET}${indicators}`; | |
| } | |
| // --- Cost --- | |
| const costStr = cost > 0 ? `$${cost.toFixed(2)}` : '$0.00'; | |
| // --- Duration --- | |
| const totalSec = Math.floor(durationMs / 1000); | |
| const mins = Math.floor(totalSec / 60); | |
| const secs = totalSec % 60; | |
| const durationStr = `${mins}m${secs.toString().padStart(2, '0')}s`; | |
| // --- Lines changed --- | |
| let linesStr = ''; | |
| if (linesAdded > 0 || linesRemoved > 0) { | |
| linesStr = `${GREEN}+${linesAdded}${RESET}/${RED}-${linesRemoved}${RESET}`; | |
| } | |
| // --- Assemble line 1: model | task | git | lines | dir --- | |
| const parts1 = [`${DIM}${model}${RESET}`]; | |
| if (taskSection) parts1.push(taskSection); | |
| if (gitSection) parts1.push(gitSection); | |
| if (linesStr) parts1.push(linesStr); | |
| parts1.push(`${DIM}${path.basename(dir)}${RESET}`); | |
| // --- Assemble line 2: context bar | usage | cost | duration --- | |
| const parts2 = []; | |
| if (ctxSection) parts2.push(ctxSection); | |
| if (usageSection) parts2.push(usageSection); | |
| parts2.push(`${DIM}${costStr}${RESET}`); | |
| parts2.push(`${DIM}${durationStr}${RESET}`); | |
| process.stdout.write(parts1.join(SEP) + '\n' + parts2.join(SEP)); | |
| } catch (e) { | |
| // Silent fail — never break the statusline | |
| } | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment