Skip to content
Agent Orchestration
Hook

Hooks

What maestro runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
maestro
23012 skills1 agent1 command7 hooks
+1
Install
> /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"
Read hooks/hooks.json

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.

Ships withmaestro

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.

Get the whole plugin
Stats
230
Stars
34
Forks
Quiet
Maintenance
JavaScript
Language
MIT
License
6mo ago
Last commit
7mo ago
Created

Repo: xenitv1/claude-code-maestro