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
196 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.jsGitHub
    Read the script
    #!/usr/bin/env node
    // claudecode-statusline — Node renderer (cross-platform, no bash/jq needed).
    // 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
    
    'use strict';
    const fs = require('fs');
    const os = require('os');
    const path = require('path');
    const { execFileSync } = require('child_process');
    
    const on = (name) => process.env[name] !== '0';
    const c = (n, s) => `\x1b[38;5;${n}m${s}\x1b[0m`;
    const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
    
    function fillBar(pct, width) {
      const filled = Math.max(0, Math.min(width, Math.floor((pct * width) / 100)));
      return '█'.repeat(filled) + '░'.repeat(width - filled);
    }
    
    function fmtReset(epoch) {
      let n = Number(epoch);
      if (!(n > 0)) return '';
      if (n > 1e12) n = Math.floor(n / 1000); // tolerate ms
      const rem = Math.max(0, n - Math.floor(Date.now() / 1000));
      const d = Math.floor(rem / 86400);
      const h = Math.floor((rem % 86400) / 3600);
      const m = Math.floor((rem % 3600) / 60);
      if (d > 0) return `${d}d ${h}h`;
      if (h > 0) return `${h}h ${m}m`;
      return `${m}m`;
    }
    
    const usedColor = (p) => (p >= 80 ? 196 : p >= 50 ? 220 : 114);
    const remColor = (p) => (p <= 20 ? 196 : p <= 50 ? 220 : 114);
    
    function readSmall(file, max = 64) {
      try {
        const st = fs.lstatSync(file);
        if (st.isSymbolicLink() || !st.isFile()) return '';
        return fs.readFileSync(file, 'utf8').slice(0, max);
      } catch (_) {
        return '';
      }
    }
    
    function rateBadge(label, node) {
      if (!node || node.used_percentage == null) return '';
      const used = Math.round(Number(node.used_percentage));
      if (Number.isNaN(used)) return '';
      const rem = 100 - used;
      const reset = fmtReset(node.resets_at);
      return (
        c(usedColor(used), `${label} ${fillBar(used, 8)} ${used}% used`) +
        ' ' +
        c(remColor(rem), `${rem}% rem`) +
        (reset ? ` ↺ ${reset}` : '')
      );
    }
    
    function render(input) {
      const parts = [];
    
      if (on('SL_CAVEMAN')) {
        const mode = readSmall(path.join(configDir, '.caveman-active'))
          .replace(/[\r\n]/g, '').toLowerCase().replace(/[^a-z0-9-]/g, '');
        const valid = ['off', 'lite', 'full', 'ultra', 'wenyan-lite', 'wenyan', 'wenyan-full', 'wenyan-ultra', 'commit', 'review', 'compress'];
        if (valid.includes(mode)) {
          parts.push(c(172, mode === 'full' ? '[🪨 CAVEMAN]' : `[🪨 CAVEMAN:${mode.toUpperCase()}]`));
        }
      }
    
      if (on('SL_PROJECT')) {
        const dir = (input.workspace && (input.workspace.project_dir || input.workspace.current_dir)) || input.cwd || process.cwd();
        let seg = c(75, `📁 ${path.basename(dir)}`);
        try {
          const branch = execFileSync('git', ['-C', dir, 'branch', '--show-current'], {
            encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 1500, windowsHide: true,
          }).trim();
          if (branch) seg += c(114, ` 🌿 ${branch}`);
        } catch (_) { /* not a repo / no git */ }
        parts.push(seg);
      }
    
      if (on('SL_MODEL') && input.model && input.model.display_name) {
        parts.push(c(183, `🤖 ${input.model.display_name}`));
      }
    
      if (on('SL_CTX') && input.context_window && input.context_window.used_percentage != null) {
        const used = Math.round(Number(input.context_window.used_percentage));
        if (!Number.isNaN(used)) parts.push(c(usedColor(used), `CTX ${fillBar(used, 8)} ${used}%`));
      }
    
      if (on('SL_EFFORT') && input.effort && input.effort.level) {
        const lvl = String(input.effort.level);
        const map = { low: [244, '▁'], medium: [75, '▄'], high: [220, '▇'], xhigh: [208, '█'], max: [196, '█'] };
        const [col, icon] = map[lvl] || [244, '?'];
        parts.push(c(col, `⚡ ${icon} ${lvl.toUpperCase()}`));
      }
    
      const rl = input.rate_limits || {};
      if (on('SL_5H')) parts.push(rateBadge('5h', rl.five_hour));
      if (on('SL_7D')) parts.push(rateBadge('7d', rl.seven_day));
    
      if (on('SL_SAVINGS') && process.env.CAVEMAN_STATUSLINE_SAVINGS !== '0') {
        const s = readSmall(path.join(configDir, '.caveman-statusline-suffix')).replace(/[\x00-\x1f]/g, '');
        if (s) parts.push(c(172, s));
      }
    
      return parts.filter(Boolean).join(c(240, ' │ '));
    }
    
    let raw = '';
    process.stdin.setEncoding('utf8');
    process.stdin.on('data', (d) => (raw += d));
    process.stdin.on('end', () => {
      let input = {};
      try { input = JSON.parse(raw.replace(/^/, '')); } catch (_) { /* render with defaults */ }
      process.stdout.write(render(input));
    });
    
  • 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.

@fahimfba
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.

Stars on GitHub

Momentum

stars in total
19
Star history of FahimFBA/claudecode-statusline on GitHub, a chart by star-history.com

More plugins that ship hooks.

superpowers

superpowers

A development methodology for coding agents that makes the agent brainstorm before building, follow plans, run parallel work and finish branches in a disciplined way. You get fewer skipped steps and more consistent results from your coding assistant.

15 skills1 hook311k
@obra@obra
Install Plugin
ponytail

ponytail

A Claude Code plugin with skills for auditing a whole repo, reviewing a change, listing deferred shortcut comments, and showing measured savings and a quick command reference.

6 skills10 hooks188k
@dietrichgebert@dietrichgebert
Install Plugin
agent-skills

agent-skills

Walks a software change from a written spec through design, testing, code review, CI/CD, and release, applying the matching engineering skill at each stage. Useful for developers using Claude Code on full development workflows.

25 skills+3112k
@addyosmani@addyosmani
Install Plugin
planning-with-files

planning-with-files

Persistent file-based planning for AI coding agents on long-running, multi-step tasks. It keeps task_plan.md, findings.md and progress.md on disk, so work can resume after /clear or context compaction.

6 skills+228k
@othmanadi@othmanadi
Install Plugin
gsd-core

gsd-core

A spec-driven development workflow for Claude Code that guides a project through planning, phases, milestone audits and fixes. Includes skills to generate tests from UAT criteria and to audit completed work.

72 skills+311k
@open-gsd@open-gsd
Install Plugin
superpowers-zh

superpowers-zh

Chinese-language skill pack for Claude Code that adds workflow guidance for brainstorming, TDD, debugging, and code review, plus Chinese conventions for commits, documentation, and domestic Git platforms like Gitee and Coding.net.

21 skills1 hook8.5k
@jnmetacode@jnmetacode
Install Plugin
socraticode

socraticode

Gives Claude Code semantic search, dependency graph and architecture context over a codebase, so an AI assistant can explore and understand a project's code. Includes skills for indexing a project and managing its file watching.

2 skills+33.4k
@giancarloerra@giancarloerra
Install Plugin
cocoindex-code

cocoindex-code

A lightweight code search engine that uses AST-based semantic search with local embeddings and incremental indexing. It helps Claude Code users find code faster and more efficiently.

1 skill1 MCP+13k
@cocoindex-io@cocoindex-io
Install Plugin

Questions about the hooks in claudecode-statusline.

Which coding agents can use the hooks in claudecode-statusline?
The hooks in claudecode-statusline install in Claude Code and Cursor: each runs Claude Code hooks from .claude/settings.json, so copy the hooks there.