Hooks
What maestro runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
> /plugin marketplace add xenitv1/claude-code-maestro > /plugin install maestro@maestro-marketplace
Ships with maestro. 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/session-start.js"
PreCompact
- Matches
*node "${CLAUDE_PLUGIN_ROOT}/hooks/pre-compact.js"node "${CLAUDE_PLUGIN_ROOT}/hooks/brain-sync.js"
SubagentStop
echo {}
Stop
node "${CLAUDE_PLUGIN_ROOT}/hooks/stop.js"node "${CLAUDE_PLUGIN_ROOT}/hooks/brain-sync.js"
PostToolUse
- Matches
*node "${CLAUDE_PLUGIN_ROOT}/hooks/brain-sync.js"
PostToolUseFailure
- Matches
*node "${CLAUDE_PLUGIN_ROOT}/hooks/brain-sync.js"
UserPromptSubmit
Fires before Claude sees each prompt you send. A plugin can use it to inject context, so the same instruction reaches the model every turn instead of only at session start.
node "${CLAUDE_PLUGIN_ROOT}/hooks/brain-sync.js"
In the plugin's words
How maestro describes its own hook set.
Maestro plugin hooks for AI memory (LTM) and Ralph Wiggum persistence
Where it lives
- hooks/brain-sync.jsRunsGitHub
Read the script
#!/usr/bin/env node /** * Brain Sync Hook - Auto-Memory System * Runs after every tool use, extracts AI memory to brain.jsonl * * @event PostToolUse */ const fs = require('fs'); const path = require('path'); const { findProjectRoot, getMaestroDir, getClaudeProjectsDir, normalizeProjectPath, ensureMaestroDir, loadState, saveState, isReadOnlyTool, logDebug, cleanAnsi, getTimestamp, readStdin, outputJson } = require('./lib/utils'); const { readPreservedBrain, writeBrain, dedupe } = require('./lib/brain'); const LOG_PREFIX = '[BRAIN-SYNC]'; // Maximum file size to read (50MB) - for full read // Files larger than this will use streaming const MAX_JSONL_SIZE = 50 * 1024 * 1024; const STREAMING_THRESHOLD = 50 * 1024 * 1024; // Use streaming for files > 50MB /** * Detect active Claude CLI session from cwd. */ function getActiveSession(projectRoot) { logDebug(LOG_PREFIX, `Project root: ${projectRoot}`); try { // STALE CONTEXT GUARD: Detect if project is empty // Only check if it's a directory (might be a newly created empty folder) if (fs.existsSync(projectRoot) && fs.lstatSync(projectRoot).isDirectory()) { const rootEntries = fs.readdirSync(projectRoot); const hasProjectFiles = rootEntries.some(e => !['.git', '.maestro', '.claude'].includes(e)); if (!hasProjectFiles) { logDebug(LOG_PREFIX, 'Project directory empty (except meta) - treated as FRESH START. Skipping legacy session recovery.'); return { sessionId: null, mainJsonl: null, subagentDir: null }; } } const claudeProjectsDir = getClaudeProjectsDir(); if (!fs.existsSync(claudeProjectsDir)) { return { sessionId: null, mainJsonl: null, subagentDir: null }; } // Normalize project path for matching const cwdNormalized = normalizeProjectPath(projectRoot); let projectDir = null; // Find matching project directory (case-insensitive for Windows compatibility) const entries = fs.readdirSync(claudeProjectsDir); // 1. Try exact match first for (const entry of entries) { if (entry.toLowerCase() === cwdNormalized.toLowerCase()) { projectDir = path.join(claudeProjectsDir, entry); break; } } // 2. Try prefix match if exact match fails (case-insensitive) if (!projectDir) { for (const entry of entries) { const entryLower = entry.toLowerCase(); const cwdLower = cwdNormalized.toLowerCase(); if (cwdLower && (entryLower.startsWith(cwdLower) || cwdLower.startsWith(entryLower))) { projectDir = path.join(claudeProjectsDir, entry); break; } } } if (!projectDir) { return { sessionId: null, mainJsonl: null, subagentDir: null }; } // Try to find session from .jsonl files const jsonlFiles = fs.readdirSync(projectDir) .filter(f => f.endsWith('.jsonl')) .map(f => ({ name: f, path: path.join(projectDir, f), mtime: fs.statSync(path.join(projectDir, f)).mtimeMs })) .sort((a, b) => b.mtime - a.mtime); if (jsonlFiles.length > 0) { const latestJsonl = jsonlFiles[0]; const sessionId = latestJsonl.name.replace('.jsonl', ''); const mainJsonl = latestJsonl.path; const subagentDir = path.join(projectDir, sessionId, 'subagents'); logDebug(LOG_PREFIX, `Session: ${sessionId}`); return { sessionId, mainJsonl, subagentDir }; } // Fallback: read sessions-index.json const indexFile = path.join(projectDir, 'sessions-index.json'); if (fs.existsSync(indexFile)) { const indexData = JSON.parse(fs.readFileSync(indexFile, 'utf-8')); const sessions = indexData.entries || []; if (sessions.length > 0) { // Get most recent session const latestSession = sessions.sort((a, b) => (b.fileMtime || 0) - (a.fileMtime || 0) )[0]; const sessionId = latestSession.sessionId; const subagentDir = path.join(projectDir, sessionId, 'subagents'); const mainJsonl = path.join(projectDir, `${sessionId}.jsonl`); logDebug(LOG_PREFIX, `Session (from index): ${sessionId}`); return { sessionId, mainJsonl, subagentDir }; } } return { sessionId: null, mainJsonl: null, subagentDir: null }; } catch (err) { logDebug(LOG_PREFIX, `Session detection error: ${err.message}`); return { sessionId: null, mainJsonl: null, subagentDir: null }; } } /** * Read JSONL file incrementally using offset tracking. */ function readJsonlIncremental(filePath, sessionId, callback) { const syncState = loadState('sync', findProjectRoot()) || {}; const fileKey = `${sessionId}:${path.basename(filePath)}`; const startOffset = syncState[fileKey] || 0; const stats = fs.statSync(filePath); if (stats.size < startOffset) { // File was rotated or cleared logDebug(LOG_PREFIX, `File ${fileKey} shrunk, resetting offset`); } const currentStart = stats.size < startOffset ? 0 : startOffset; if (currentStart >= stats.size) { logDebug(LOG_PREFIX, `No new content in ${fileKey}`); return Promise.resolve(); } logDebug(LOG_PREFIX, `Reading ${fileKey} from offset ${currentStart}`); // Open file for reading const fd = fs.openSync(filePath, 'r'); const bufferSize = 64 * 1024; const buffer = Buffer.alloc(bufferSize); let bytesRead; let leftover = ''; let currentOffset = currentStart; while ((bytesRead = fs.readSync(fd, buffer, 0, bufferSize, currentOffset)) > 0) { currentOffset += bytesRead; const chunk = leftover + buffer.toString('utf-8', 0, bytesRead); const lines = chunk.split('\n'); leftover = lines.pop(); // Last line might be incomplete for (const line of lines) { if (line.trim()) { try { callback(JSON.parse(line)); } catch (err) { /* Skip invalid */ } } } } fs.closeSync(fd); // Save new offset syncState[fileKey] = currentOffset; s - hooks/pre-compact.jsRunsGitHub
Read the script
#!/usr/bin/env node /** * Pre-Compact Hook - Compact Summary Capture * Captures compact summaries before/after manual /compact command * and persists them to brain.jsonl for cross-session memory. * * @event PreCompact * @matcher manual - Triggered by /compact command */ const fs = require('fs'); const path = require('path'); const { findProjectRoot, getMaestroDir, ensureMaestroDir, getClaudeProjectsDir, normalizeProjectPath, loadState, saveState, logDebug, readStdin, outputJson } = require('./lib/utils'); const { readBrain, writeBrain, appendToBrain, extractLastSummary, writeCompactToBrain } = require('./lib/brain'); const LOG_PREFIX = '[PRE-COMPACT]'; /** * Find the current active session transcript. */ function getCurrentTranscript() { try { const claudeProjectsDir = getClaudeProjectsDir(); if (!fs.existsSync(claudeProjectsDir)) { return null; } const projectRoot = findProjectRoot(); const cwdNormalized = normalizeProjectPath(projectRoot); let projectDir = null; // Find matching project directory (case-insensitive for Windows compatibility) const entries = fs.readdirSync(claudeProjectsDir); for (const entry of entries) { if (entry.toLowerCase() === cwdNormalized.toLowerCase()) { projectDir = path.join(claudeProjectsDir, entry); break; } } // Prefix match fallback (case-insensitive) if (!projectDir) { for (const entry of entries) { const entryLower = entry.toLowerCase(); const cwdLower = cwdNormalized.toLowerCase(); if (cwdLower && (entryLower.startsWith(cwdLower) || cwdLower.startsWith(entryLower))) { projectDir = path.join(claudeProjectsDir, entry); break; } } } if (!projectDir) { return null; } // Find most recent JSONL file const jsonlFiles = fs.readdirSync(projectDir) .filter(f => f.endsWith('.jsonl')) .map(f => ({ name: f, path: path.join(projectDir, f), mtime: fs.statSync(path.join(projectDir, f)).mtimeMs })) .sort((a, b) => b.mtime - a.mtime); if (jsonlFiles.length > 0) { return jsonlFiles[0].path; } return null; } catch (err) { logDebug(LOG_PREFIX, `Error finding transcript: ${err.message}`); return null; } } /** * Main hook entry point. */ async function main() { const projectRoot = findProjectRoot(); logDebug(LOG_PREFIX, '='.repeat(60)); logDebug(LOG_PREFIX, 'PRE-COMPACT HOOK TRIGGERED'); logDebug(LOG_PREFIX, `Project Root: ${projectRoot}`); try { // Read hook input const hookInput = await readStdin(); logDebug(LOG_PREFIX, `Hook input: trigger=${hookInput.trigger}, custom_instructions=${hookInput.custom_instructions ? 'yes' : 'no'}`); // Only process manual compacts if (hookInput.trigger !== 'manual') { outputJson({}); return; } // Get current transcript const transcriptPath = hookInput.transcript_path || getCurrentTranscript(); logDebug(LOG_PREFIX, `Current transcript: ${transcriptPath ? path.basename(transcriptPath) : 'none'}`); if (!transcriptPath) { outputJson({}); return; } // Extract last summary from transcript const summary = extractLastSummary(transcriptPath); logDebug(LOG_PREFIX, `Summary found: ${summary ? 'yes (' + summary.length + ' chars)' : 'no'}`); if (summary) { // Write to brain.jsonl writeCompactToBrain(summary, projectRoot); // Also save to state for reference if needed saveState('last-compact', { summary: summary.substring(0, 5000), timestamp: Date.now(), trigger: 'manual' }, projectRoot); // Output context for the new session outputJson({ type: 'compact_capture', hookSpecificOutput: { hookEventName: 'PreCompact', additionalContext: ` ✅ Current summary captured and saved to brain.jsonl. Note: Compaction will now continue and a new summary will be generated. ` } }); logDebug(LOG_PREFIX, 'Pre-compact hook completed successfully'); } else { // It's normal to not find a summary in PreCompact because it hasn't been generated yet for this compaction. // But we output OK anyway. outputJson({}); logDebug(LOG_PREFIX, 'No pre-existing compact summary found in current transcript.'); } } catch (err) { logDebug(LOG_PREFIX, `Hook error: ${err.message}`); logDebug(LOG_PREFIX, `Stack: ${err.stack}`); outputJson({}); } } main(); - hooks/session-start.jsRunsGitHub
Read the script
#!/usr/bin/env node /** * Session Start Hook for Maestro Plugin * Reads brain.jsonl and project context when a new session begins. * Analyzes package.json for tech stack info and stores in LTM. * * @event SessionStart */ const fs = require('fs'); const path = require('path'); const { findProjectRoot, getMaestroDir, ensureMaestroDir, readFileSafe, getFileHash, readStdin, logDebug, outputJson } = require('./lib/utils'); const { formatBrainForContext, writeTechToBrain, extractLastSummary, writeCompactToBrain } = require('./lib/brain'); const LOG_PREFIX = '[SESSION-START]'; // Framework detection patterns const FRAMEWORK_PATTERNS = { 'next.js': { deps: ['next'], files: ['next.config.js', 'next.config.mjs', 'next.config.ts'] }, 'react': { deps: ['react', 'react-dom'], files: [] }, 'vue': { deps: ['vue'], files: ['vue.config.js', 'nuxt.config.js'] }, 'nuxt': { deps: ['nuxt'], files: ['nuxt.config.js', 'nuxt.config.ts'] }, 'angular': { deps: ['@angular/core'], files: ['angular.json'] }, 'svelte': { deps: ['svelte'], files: ['svelte.config.js'] }, 'express': { deps: ['express'], files: [] }, 'fastify': { deps: ['fastify'], files: [] }, 'nestjs': { deps: ['@nestjs/core'], files: ['nest-cli.json'] }, 'electron': { deps: ['electron'], files: [] }, 'tauri': { deps: ['@tauri-apps/api'], files: ['tauri.conf.json'] }, 'astro': { deps: ['astro'], files: ['astro.config.mjs'] }, 'remix': { deps: ['@remix-run/react'], files: ['remix.config.js'] }, 'gatsby': { deps: ['gatsby'], files: ['gatsby-config.js'] } }; // Important dependencies to track const IMPORTANT_DEPS = { // State management 'zustand': 'State (Zustand)', 'redux': 'State (Redux)', 'recoil': 'State (Recoil)', 'jotai': 'State (Jotai)', 'mobx': 'State (MobX)', '@tanstack/react-query': 'Data Fetching (React Query)', 'swr': 'Data Fetching (SWR)', // Styling 'tailwindcss': 'Styling (Tailwind)', 'styled-components': 'Styling (Styled Components)', '@emotion/react': 'Styling (Emotion)', 'sass': 'Styling (SASS)', '@mui/material': 'UI (Material UI)', '@chakra-ui/react': 'UI (Chakra)', 'antd': 'UI (Ant Design)', 'shadcn-ui': 'UI (shadcn)', // Database 'prisma': 'ORM (Prisma)', '@prisma/client': 'ORM (Prisma)', 'drizzle-orm': 'ORM (Drizzle)', 'typeorm': 'ORM (TypeORM)', 'mongoose': 'ODM (Mongoose)', 'sequelize': 'ORM (Sequelize)', // Auth 'next-auth': 'Auth (NextAuth)', '@clerk/nextjs': 'Auth (Clerk)', '@supabase/supabase-js': 'Backend (Supabase)', 'firebase': 'Backend (Firebase)', // Testing 'jest': 'Testing (Jest)', 'vitest': 'Testing (Vitest)', '@testing-library/react': 'Testing (RTL)', 'playwright': 'E2E (Playwright)', 'cypress': 'E2E (Cypress)', // Build tools 'vite': 'Build (Vite)', 'webpack': 'Build (Webpack)', 'esbuild': 'Build (esbuild)', 'turbo': 'Monorepo (Turborepo)', // Utilities 'zod': 'Validation (Zod)', 'yup': 'Validation (Yup)', 'axios': 'HTTP (Axios)', 'date-fns': 'Dates (date-fns)', 'dayjs': 'Dates (Day.js)', 'lodash': 'Utils (Lodash)', 'framer-motion': 'Animation (Framer)' }; /** * Check if tech stack needs re-analysis (package.json changed). */ function shouldReanalyzeTech(projectRoot) { const pkgPath = path.join(projectRoot, 'package.json'); const hashFile = path.join(getMaestroDir(projectRoot), '.tech_hash'); const currentHash = getFileHash(pkgPath); if (!currentHash) { return false; // No package.json } if (fs.existsSync(hashFile)) { try { const storedHash = fs.readFileSync(hashFile, 'utf-8').trim(); if (storedHash === currentHash) { return false; // No change } } catch (err) { // Continue with reanalysis } } return true; } /** * Save current package.json hash. */ function saveTechHash(projectRoot) { const pkgPath = path.join(projectRoot, 'package.json'); const hashFile = path.join(ensureMaestroDir(projectRoot), '.tech_hash'); const hash = getFileHash(pkgPath); if (hash) { try { fs.writeFileSync(hashFile, hash, 'utf-8'); } catch (err) { logDebug(LOG_PREFIX, `Error saving tech hash: ${err.message}`); } } } /** * Analyze package.json and extract tech stack info. */ function analyzePackageJson(projectRoot) { const pkgPath = path.join(projectRoot, 'package.json'); if (!fs.existsSync(pkgPath)) { logDebug(LOG_PREFIX, 'No package.json found'); return null; } try { const pkgContent = fs.readFileSync(pkgPath, 'utf-8'); const pkg = JSON.parse(pkgContent); const result = { name: pkg.name || 'unknown', version: pkg.version || '0.0.0', description: pkg.description || '', frameworks: [], frameworkVersions: {}, keyDeps: [], devTools: [], scripts: {}, nodeVersion: null, packageManager: null, type: pkg.type || 'commonjs' }; // Combine all dependencies const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }; // Detect frameworks for (const [framework, patterns] of Object.entries(FRAMEWORK_PATTERNS)) { // Check dependencies for (const dep of patterns.deps) { if (allDeps[dep]) { if (!result.frameworks.includes(framework)) { result.frameworks.push(framework); } // Capture version const version = allDeps[dep].replace(/[\^~>=<]/g, ''); result.frameworkVersions[dep] = version; break; } } // Check config files for (const cfgFile of patterns.files) { if (fs.existsSync(path.join(projectRoot, cfgFile))) { if (!result.frameworks.includes(framework)) { result.frameworks.push(framework); } break; } } } // Always capture React version if present if (allDeps['react'] && !result.frameworkVersions['react']) { result.frameworkVersions['react'] = allDeps['reac - hooks/stop.jsRunsGitHub
Read the script
#!/usr/bin/env node /** * Stop Hook for Maestro Plugin (Ralph Wiggum Controller) * Controls whether Claude can exit or must continue iterating. * * @event Stop, SubagentStop */ const { findProjectRoot, logDebug, readStdin, outputJson } = require('./lib/utils'); const Ralph = require('./lib/ralph'); const LOG_PREFIX = '[STOP]'; /** * Main hook entry point. */ async function main() { const projectRoot = findProjectRoot(); logDebug(LOG_PREFIX, '='.repeat(60)); logDebug(LOG_PREFIX, 'STOP HOOK TRIGGERED'); try { const hookInput = await readStdin(); const inputData = JSON.stringify(hookInput); // Check if this is a SubagentStop event const hookEvent = hookInput.hookEventName || ''; const isSubagentStop = hookEvent === 'SubagentStop' || inputData.toLowerCase().includes('subagent'); if (isSubagentStop) { logDebug(LOG_PREFIX, 'SUBAGENT STOP detected - allowing subagent to complete'); outputJson({}); process.exit(0); return; } // Check Ralph Wiggum iteration state const decision = Ralph.getBlockDecision(); if (!decision.block) { // Allow exit logDebug(LOG_PREFIX, `Exit allowed: ${decision.reason}`); // Cleanup if completed if (decision.completed) { logDebug(LOG_PREFIX, 'Ralph Wiggum completed - cleaning up state'); Ralph.clearState(); } outputJson({}); process.exit(0); return; } // Block exit - Ralph wants more iterations logDebug(LOG_PREFIX, `Exit BLOCKED: ${decision.reason} (${decision.current}/${decision.max})`); // Build continuation message const continuationMessage = ` 🔄 RALPH WIGGUM 2.0: ELITE PERSISTENCE ACTIVE ## 📊 Iteration Status **Progress:** ${decision.current} / ${decision.max} ## ⚠️ TASK COMPLETION BLOCKED Ralph Wiggum requires more iterations to ensure quality standards are met. ### Next Steps: 1. Continue testing and fixing issues 2. Run tests again to verify fixes 3. Check verification matrix coverage 4. Ensure all critical tests pass The task will remain blocked until: - All tests pass OR - Maximum iterations reached OR - Manual completion signal received --- **To manually complete:** Create .maestro/ralph.complete file **To stop early:** Delete .maestro/ralph.active file `; // Block the exit with continuation prompt outputJson({ block: true, message: continuationMessage.trim(), iteration: decision.current, maxIterations: decision.max }); process.exit(0); } catch (err) { logDebug(LOG_PREFIX, `Error: ${err.message}`); outputJson({}); } } main();
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.
Elite-tier orchestration framework for Claude Code CLI. Supercharges AI development through specialized agents, modular skills, intelligent hooks, and persistent memory systems. Author: xenitV1 • X/Twitter Philosophy: "Why over How.
Repo: xenitv1/claude-code-maestro

