fix(acr): conflict-detector segregates plugin-bundled configs (M-BUG-2)
CNF compared every discovered settings.json/hooks.json pairwise regardless of origin, so it treated installed plugins' bundled configs — each plugin's own settings.json/hooks.json plus its shipped test fixtures and examples under ~/.claude/plugins/ — as if they were the user's authored cascade. A "conflict" between two plugins' bundled test fixtures is not something a user can resolve, yet these dominated the count: 339 CNF findings on this machine (315 high-sev permission allow/deny "conflicts", 18 duplicate-hook, 6 settings-key), almost all sourced from plugin-internal fixtures. The Conflicts grade was F on pure noise. Fix: CNF excludes any file whose path is under `.claude/plugins/` from conflict analysis (new isPluginBundled predicate; absPath marker). Kept CNF-local rather than a discovery-level skip on purpose: an active plugin's contributed hooks.json/.mcp.json legitimately lives in plugins/cache and other scanners need it — only conflict analysis must ignore plugin-bundled files. Same class as M-BUG-8 (non-live config trees treated as live). Suite 1344/0 (+3: plugin-bundled exclusion, discovery-side sanity, over-exclusion guard). Frozen v5.0.0 + SC-5 snapshots untouched (marketplace-medium has no plugins/ paths), no re-seed. Dogfood ~/.claude CNF 339->0 (F-grade was 100% plugin-bundled noise; the ~3 genuine user-scope local settings have no actual conflicting keys, matching the plan C5 "real surface ~3 files" prediction). Follow-up (not in this fix): classifyScope tags plugin-bundled files by checking basePath instead of the file's own path, so scope:'plugin' is effectively dead for a ~/.claude-rooted scan. Fixing it would let every scanner trust the scope field, but that is a discovery-layer change beyond this bug's scope.
This commit is contained in:
parent
3cf5c714a2
commit
e8afb148d3
2 changed files with 117 additions and 5 deletions
|
|
@ -5,6 +5,7 @@
|
|||
* Finding IDs: CA-CNF-NNN
|
||||
*/
|
||||
|
||||
import { sep } from 'node:path';
|
||||
import { readTextFile } from './lib/file-discovery.mjs';
|
||||
import { finding, scannerResult } from './lib/output.mjs';
|
||||
import { SEVERITY } from './lib/severity.mjs';
|
||||
|
|
@ -17,6 +18,22 @@ const SCANNER = 'CNF';
|
|||
// Keys checked separately or not meaningful to compare
|
||||
const SKIP_KEYS = new Set(['$schema', 'hooks', 'permissions']);
|
||||
|
||||
// Files under `.claude/plugins/` are shipped by installed plugins — the plugin's
|
||||
// own settings.json/hooks.json plus bundled test fixtures and examples. They are
|
||||
// not the user's authored cascade and a "conflict" between them is not something
|
||||
// the user can resolve, so they must be excluded from cross-scope conflict
|
||||
// analysis. (Other scanners still need active plugin config, so this exclusion is
|
||||
// CNF-local, not a discovery-level skip. M-BUG-2.)
|
||||
const PLUGIN_TREE_MARKER = `.claude${sep}plugins${sep}`;
|
||||
|
||||
/**
|
||||
* @param {import('./lib/file-discovery.mjs').ConfigFile} file
|
||||
* @returns {boolean} true if the file is shipped by an installed plugin
|
||||
*/
|
||||
function isPluginBundled(file) {
|
||||
return file.absPath.includes(PLUGIN_TREE_MARKER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten an object's top-level keys into a simple key→value map.
|
||||
* Only first level — we compare top-level settings, not nested.
|
||||
|
|
@ -63,10 +80,10 @@ export async function scan(targetPath, discovery) {
|
|||
const start = Date.now();
|
||||
const findings = [];
|
||||
|
||||
// Collect settings files
|
||||
const settingsFiles = discovery.files.filter(f => f.type === 'settings-json');
|
||||
// Collect hooks files
|
||||
const hooksFiles = discovery.files.filter(f => f.type === 'hooks-json');
|
||||
// Collect settings files (excluding plugin-bundled — see PLUGIN_TREE_MARKER)
|
||||
const settingsFiles = discovery.files.filter(f => f.type === 'settings-json' && !isPluginBundled(f));
|
||||
// Collect hooks files (excluding plugin-bundled)
|
||||
const hooksFiles = discovery.files.filter(f => f.type === 'hooks-json' && !isPluginBundled(f));
|
||||
|
||||
const totalFiles = settingsFiles.length + hooksFiles.length;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue