This commit is contained in:
2026-04-12 01:06:31 +07:00
commit 10d660cbcb
1066 changed files with 228596 additions and 0 deletions

View File

@@ -0,0 +1,264 @@
#!/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
};