Skip to content
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.

From plugin
claudecode-statusline
155 hooks

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