#!/usr/bin/env node /** * broad-pattern-detector.cjs - Detect overly broad glob patterns * * Prevents LLMs from filling context by using patterns like "all files" * at project root, which returns ALL files of a type. * * Detection Strategy: * 1. Pattern breadth: recursive glob at start = recursive everywhere * 2. Path depth: Root or shallow paths are high-risk * 3. Combined: Broad pattern + high-level path = BLOCK */ const path = require('path'); // Patterns that recursively match everywhere when at root // These are dangerous because they return ALL matching files const BROAD_PATTERN_REGEXES = [ // ** - all files everywhere (no filter at all) /^\*\*$/, // * - all files in root /^\*$/, // **/* - all files everywhere /^\*\*\/\*$/, // **/. - all dotfiles everywhere /^\*\*\/\.\*$/, // *.ext at root (matches all in root, but combined with deep search) /^\*\.\w+$/, // *.{ext,ext2} at root /^\*\.\{[^}]+\}$/, // **/*.ext - all files of type everywhere (e.g., **/*.ts, **/*.js) /^\*\*\/\*\.\w+$/, // **/*.{ext,ext2} - all files of multiple types everywhere /^\*\*\/\*\.\{[^}]+\}$/, ]; // Common source directories that indicate a more specific search const SPECIFIC_DIRS = [ 'src', 'lib', 'app', 'apps', 'packages', 'components', 'pages', 'api', 'server', 'client', 'web', 'mobile', 'shared', 'common', 'utils', 'helpers', 'services', 'hooks', 'store', 'routes', 'models', 'controllers', 'views', 'tests', '__tests__', 'spec' ]; // High-risk paths (project/worktree roots) const HIGH_RISK_INDICATORS = [ // Worktree paths /\/worktrees\/[^/]+\/?$/, // Project roots (contain package.json, etc.) /^\.?\/?$/, // Shallow paths (just one directory deep) /^[^/]+\/?$/ ]; /** * Check if a glob pattern is overly broad * * @param {string} pattern - The glob pattern to check * @returns {boolean} */ function isBroadPattern(pattern) { if (!pattern || typeof pattern !== 'string') return false; const normalized = pattern.trim(); // Check against known broad patterns for (const regex of BROAD_PATTERN_REGEXES) { if (regex.test(normalized)) { return true; } } return false; } /** * Check if pattern contains a specific subdirectory. * Scoped patterns like "src/..." are OK because they target specific dirs. * * @param {string} pattern - The glob pattern * @returns {boolean} */ function hasSpecificDirectory(pattern) { if (!pattern) return false; // Check if pattern starts with a specific directory for (const dir of SPECIFIC_DIRS) { if (pattern.startsWith(`${dir}/`) || pattern.startsWith(`./${dir}/`)) { return true; } } // Check for any non-glob directory prefix // e.g., "mydir/..." has a specific directory const firstSegment = pattern.split('/')[0]; if (firstSegment && !firstSegment.includes('*') && firstSegment !== '.') { return true; } return false; } /** * Check if the base path is at a high-level (risky) location * * @param {string} basePath - The path where glob will run * @param {string} cwd - Current working directory * @returns {boolean} */ function isHighLevelPath(basePath, cwd) { // No path specified = uses CWD (often project root) if (!basePath) return true; const normalized = basePath.replace(/\\/g, '/'); // Check high-risk indicators for (const regex of HIGH_RISK_INDICATORS) { if (regex.test(normalized)) { return true; } } // Check path depth - shallow paths are higher risk const segments = normalized.split('/').filter(s => s && s !== '.'); if (segments.length <= 1) { return true; } // If path doesn't contain a specific directory, it's high-level const hasSpecific = SPECIFIC_DIRS.some(dir => normalized.includes(`/${dir}/`) || normalized.includes(`/${dir}`) || normalized.startsWith(`${dir}/`) || normalized === dir ); return !hasSpecific; } /** * Generate suggestions for more specific patterns * * @param {string} pattern - The broad pattern * @returns {string[]} */ function suggestSpecificPatterns(pattern) { const suggestions = []; // Extract the extension/file part from the pattern let ext = ''; const extMatch = pattern.match(/\*\.(\{[^}]+\}|\w+)$/); if (extMatch) { ext = extMatch[1]; } // Suggest common directories const commonDirs = ['src', 'lib', 'app', 'components']; for (const dir of commonDirs) { if (ext) { suggestions.push(`${dir}/**/*.${ext}`); } else { suggestions.push(`${dir}/**/*`); } } // If it's a TypeScript pattern, add specific suggestions if (pattern.includes('.ts') || pattern.includes('{ts')) { suggestions.unshift('src/**/*.ts', 'src/**/*.tsx'); } // If it's a JavaScript pattern if (pattern.includes('.js') || pattern.includes('{js')) { suggestions.unshift('src/**/*.js', 'lib/**/*.js'); } return suggestions.slice(0, 4); // Return top 4 suggestions } /** * Main detection function - check if a Glob tool call is problematic * * @param {Object} toolInput - The tool_input from hook JSON * @param {string} toolInput.pattern - The glob pattern * @param {string} [toolInput.path] - Optional base path * @returns {Object} { blocked: boolean, reason?: string, suggestions?: string[] } */ function detectBroadPatternIssue(toolInput) { if (!toolInput || typeof toolInput !== 'object') { return { blocked: false }; } const { pattern, path: basePath } = toolInput; // No pattern = nothing to check if (!pattern) { return { blocked: false }; } // Pattern has a specific directory = OK if (hasSpecificDirectory(pattern)) { return { blocked: false }; } // Check if pattern is broad if (!isBroadPattern(pattern)) { return { blocked: false }; } // Check if path is high-level if (!isHighLevelPath(basePath)) { return { blocked: false }; } // Broad pattern at high-level path = BLOCK return { blocked: true, reason: `Pattern '${pattern}' is too broad for ${basePath || 'project root'}`, pattern: pattern, suggestions: suggestSpecificPatterns(pattern) }; } /** * Format error message for broad pattern detection * * @param {Object} result - Result from detectBroadPatternIssue * @param {string} claudeDir - Path to .claude directory * @returns {string} */ function formatBroadPatternError(result, claudeDir) { const { reason, pattern, suggestions } = result; const lines = [ '', '\x1b[36mNOTE:\x1b[0m This is not an error - this block is intentional to optimize context.', '', '\x1b[31mBLOCKED\x1b[0m: Overly broad glob pattern detected', '', ` \x1b[33mPattern:\x1b[0m ${pattern}`, ` \x1b[33mReason:\x1b[0m Would return ALL matching files, filling context`, '', ' \x1b[34mUse more specific patterns:\x1b[0m', ]; for (const suggestion of suggestions || []) { lines.push(` • ${suggestion}`); } lines.push(''); lines.push(' \x1b[2mTip: Target specific directories to avoid context overflow\x1b[0m'); lines.push(''); return lines.join('\n'); } module.exports = { isBroadPattern, hasSpecificDirectory, isHighLevelPath, suggestSpecificPatterns, detectBroadPatternIssue, formatBroadPatternError, BROAD_PATTERN_REGEXES, SPECIFIC_DIRS, HIGH_RISK_INDICATORS };