Hooks
What claude-dashboard runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
> /plugin marketplace add uppinote20/claude-dashboard > /plugin install claude-dashboard@claude-dashboard
Ships with claude-dashboard. Installing the plugin gets these hooks.
What fires, and when
SessionStart
Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.
node "${CLAUDE_PLUGIN_ROOT}/hooks/ensure-statusline.mjs"
In the plugin's words
How claude-dashboard describes its own hook set.
Keeps statusLine pointed at the version-agnostic shim. Each SessionStart array element is a matcher group whose own 'hooks' array holds the commands, so the inner nesting is required by the schema rather than redundant.
Where it lives
- hooks/ensure-statusline.mjsRunsGitHub
Read the script
#!/usr/bin/env node /** * SessionStart hook: install/refresh the status line shim and migrate settings.json * off this plugin's version-pinned path. * * @handbook 4.8-version-agnostic-statusline * @tested scripts/__tests__/ensure-statusline.test.ts */ import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, realpathSync, statSync, unlinkSync, chmodSync, } from 'fs'; import path from 'path'; import { homedir } from 'os'; import { fileURLToPath } from 'url'; // Deliberately not exported: the tests pin this filename as a literal instead, because it // is a published contract — commands/setup.md writes the same string by hand and users' // settings.json points at it. A test importing this constant would keep passing if the // value changed while the doc snippet did not. const SHIM_FILENAME = 'statusline.mjs'; /** * Write to a temp file and rename, so a reader never sees a half-written file: a status * line render in another session may load the shim at this instant, and a crashed write * must not truncate settings.json. The temp name carries the pid so concurrent * SessionStart hooks — several sessions launching at once, e.g. after a restart — don't * interleave writes to one shared temp path. * * When `mode` is given it is applied twice on purpose: writeFileSync's option is * umask-filtered and only approximates the requested mode, so chmodSync afterward makes * it exact. That chmod is best-effort — failing to restore permissions must not abort the * write it was decorating. */ function atomicWrite(target, content, mode) { const tmp = `${target}.${process.pid}.tmp`; try { writeFileSync(tmp, content, mode === undefined ? {} : { mode }); if (mode !== undefined) { try { chmodSync(tmp, mode); } catch { // Best-effort; see above. } } renameSync(tmp, target); } catch (err) { try { unlinkSync(tmp); } catch { // Best-effort cleanup; the original error is what matters. } throw err; } } /** * Copy the bundled shim template into the plugin's persistent data directory. * Writing only on difference keeps the hook a no-op in the common case, and lets a * future plugin version upgrade the shim itself. */ export function syncShim(pluginRoot, pluginData) { const source = path.join(pluginRoot, 'scripts', 'statusline-shim.mjs'); if (!existsSync(source)) return null; const dest = path.join(pluginData, SHIM_FILENAME); const content = readFileSync(source, 'utf8'); if (existsSync(dest) && readFileSync(dest, 'utf8') === content) return dest; mkdirSync(pluginData, { recursive: true }); atomicWrite(dest, content); return dest; } /** * Matches only this plugin's version-pinned command. Three branches, not a blanket wildcard, * to avoid swallowing flags (`--inspect`) or wrapper arguments (`my-wrapper.js`): * - Double-quoted path (can contain spaces, typical Windows) * - Single-quoted path (can contain spaces) * - Unquoted path (no spaces allowed, just non-whitespace before the pinned path tail) * Any other form — flags, arguments, wrappers — leaves the command alone. */ const PINNED_TAIL = String.raw`[/\\]plugins[/\\]cache[/\\]claude-dashboard[/\\]claude-dashboard[/\\]\d+\.\d+\.\d+[/\\]dist[/\\]index\.js`; const PINNED_COMMAND = new RegExp( `^\\s*node\\s+(?:"[^"]*${PINNED_TAIL}"|'[^']*${PINNED_TAIL}'|(?!["'])\\S*${PINNED_TAIL})\\s*$` ); /** * Point statusLine.command at the stable shim, but only when it currently holds this * plugin's pinned path. Anything else — a user-authored line, another tool, an * already-migrated path — is left alone. */ export function migrateStatusLine(settingsPath, shimPath) { if (!existsSync(settingsPath)) return 'no-settings'; let raw; let settings; try { raw = readFileSync(settingsPath, 'utf8'); settings = JSON.parse(raw); } catch { return 'unparsable'; } const current = settings?.statusLine?.command; if (typeof current !== 'string' || !PINNED_COMMAND.test(current)) return 'skipped'; // Resolve the real file before writing: settingsPath may traverse a symlink (a // dotfiles-managed ~/.claude/settings.json), and the backup + rewrite must land on the // file the symlink actually points at, not replace the symlink with a plain copy. // (The existsSync check above already returned for a missing file, so settingsPath is // known to exist here.) const target = realpathSync(settingsPath); // The mode is preserved explicitly so a deliberately-restricted settings.json (env // secrets, apiKeyHelper) doesn't widen on migration, and the .bak copy stays exactly as // private as the source it was copied from. atomicWrite applies it; see there for why // that takes both a writeFileSync option and a follow-up chmod. const mode = statSync(target).mode; // Atomic here too, not just tidiness: the `!existsSync` guard means a backup truncated // by a crashed write would be mistaken for a good one and never rewritten, losing the // original for good. const backup = `${target}.bak`; if (!existsSync(backup)) atomicWrite(backup, raw, mode); // Quote unconditionally: a space-only check misses shell metacharacters (e.g. a config // dir at `/home/user(a)/.claude`), which `sh` then fails to parse since statusLine.command // is shell-evaluated. JSON.stringify always quotes and correctly escapes embedded quotes // and backslashes too. settings.statusLine.command = `node ${JSON.stringify(shimPath)}`; // JSON.stringify(settings, null, 2) reformats the whole file to 2-space indentation — // a hand-maintained settings.json with different spacing gets rewritten to match. This // is an accepted trade-off; the `.bak` above preserves the original exactly. atomicWrite(target, `${JSON.stringify(settings, null, 2)}\n`, mode); return 'migrated'; } // Guarded so the test suite can import the functions without running the hook. // Realpath comparison: settingsPath.command m
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.
Comprehensive status line plugin for Claude Code — unified usage monitoring across Claude, Codex, Gemini, Antigravity, and z.ai CLIs, with context, rate limits, cost tracking, and a modular widget system.
Repo: uppinote20/claude-dashboard

