#!/usr/bin/env node // ponytail — Claude Code SessionStart activation hook // // Runs on every session start: // 1. Writes flag file at ~/.claude/.ponytail-active (statusline reads this) // 2. Emits ponytail 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 } = require('./ponytail-config'); const { clearMode, isCodex, setMode, writeHookOutput, } = require('./ponytail-runtime'); const claudeDir = path.join(os.homedir(), '.claude'); 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') { clearMode(); writeHookOutput('SessionStart', 'off', isCodex ? '' : 'OK'); process.exit(0); } // 1. Write flag file try { setMode(mode); } catch (e) { // Silent fail -- flag is best-effort, don't block the hook } // 2. Emit the ponytail ruleset, filtered to the active intensity level. // A short summary is too weak — models drift back to over-building // mid-conversation, especially after context compression prunes it. // 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 intensity levels. // For these, emit a short activation line; the skill itself handles behavior. const INDEPENDENT_MODES = new Set(['review']); if (INDEPENDENT_MODES.has(mode)) { writeHookOutput( 'SessionStart', mode, 'PONYTAIL MODE ACTIVE — level: ' + mode + '. Behavior defined by /ponytail-' + mode + ' skill.', ); process.exit(0); } // Read SKILL.md — the single source of truth for ponytail behavior. // Plugin installs: __dirname = /hooks/, SKILL.md at /skills/ponytail/SKILL.md let skillContent = ''; try { skillContent = fs.readFileSync( path.join(__dirname, '..', 'skills', 'ponytail', '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) { if (tableRowMatch[1] === mode) { 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] === mode) { acc.push(line); } return acc; } acc.push(line); return acc; }, []); output = 'PONYTAIL MODE ACTIVE — level: ' + mode + '\n\n' + filtered.join('\n'); } else { // Fallback when SKILL.md is not found (hook installed without skills dir). // Minimum viable ruleset — better than nothing. output = 'PONYTAIL MODE ACTIVE — level: ' + mode + '\n\n' + 'You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.\n\n' + '## Persistence\n\n' + 'ACTIVE EVERY RESPONSE. No drift back to over-building. Still active if unsure. Off only: "stop ponytail" / "normal mode".\n\n' + 'Current level: **' + mode + '**. Switch: `/ponytail lite|full|ultra`.\n\n' + '## The ladder\n\n' + 'Before any code, stop at the first rung that holds:\n' + '1. Does this need to be built at all? (YAGNI)\n' + '2. Does the standard library do this? Use it.\n' + '3. Does a native platform feature cover it? Use it.\n' + '4. Does an already-installed dependency solve it? Use it.\n' + '5. Can this be one line? Make it one line.\n' + '6. Only then: write the minimum code that works.\n\n' + '## Rules\n\n' + 'No abstractions that were not requested. No avoidable dependencies. No boilerplate nobody asked for. ' + 'Deletion over addition. Boring over clever. Fewest files possible. ' + 'Ship the lazy version and question the complex request in the same response — never stall. ' + 'Between two same-size stdlib options, pick the one correct on edge cases. ' + 'Mark intentional simplifications with a `ponytail:` comment — a shortcut with a known ceiling names the ceiling and the upgrade path in the comment.\n\n' + '## Output\n\n' + 'Code first. Then at most three short lines: what was skipped, when to add it. ' + 'If the explanation is longer than the code, delete the explanation.\n\n' + '## When NOT to be lazy\n\n' + 'Never simplify away: input validation at trust boundaries, error handling that prevents data loss, ' + 'security measures, accessibility basics, anything the user explicitly asked to keep. ' + 'Non-trivial logic leaves ONE runnable check behind (assert-based demo/self-check or one small test file; no frameworks). Trivial one-liners need no test.\n\n' + '## Boundaries\n\n' + 'Ponytail governs what you build, not how you talk. "stop ponytail" or "normal mode": revert. Level persists until changed or session end.'; } // 3. Detect missing statusline config — nudge Claude to help set it up if (!isCodex) 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 ? 'ponytail-statusline.ps1' : 'ponytail-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 ponytail plugin includes a statusline badge showing active mode " + "(e.g. [PONYTAIL], [PONYTAIL:ULTRA]). It is not configured yet. " + "To enable, add this to ~/.claude/settings.json: " + statusLineSnippet + " " + "Proactively offer to set this up for the user on first interaction."; } } catch (e) { // Silent fail — don't block session start over statusline detection } writeHookOutput('SessionStart', mode, output);