From 9330124f5c2483d14dcb1985f66c220c7b17a427 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Fri, 1 May 2026 06:50:24 +0200 Subject: [PATCH] feat(config-audit): flag additionalDirectories > 2 (v5 M6) [skip-docs] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 'additionalDirectories' to KNOWN_KEYS - Emit low severity finding when length > 2 - New fixtures: additional-dirs-many (3 entries) + additional-dirs-ok (2) 569 → 572 tests, all green. --- .../scanners/settings-validator.mjs | 25 ++++++++++++++ .../.claude/settings.json | 8 +++++ .../additional-dirs-ok/.claude/settings.json | 7 ++++ .../scanners/settings-validator.test.mjs | 33 +++++++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 plugins/config-audit/tests/fixtures/additional-dirs-many/.claude/settings.json create mode 100644 plugins/config-audit/tests/fixtures/additional-dirs-ok/.claude/settings.json diff --git a/plugins/config-audit/scanners/settings-validator.mjs b/plugins/config-audit/scanners/settings-validator.mjs index 5e053b6..405bbe3 100644 --- a/plugins/config-audit/scanners/settings-validator.mjs +++ b/plugins/config-audit/scanners/settings-validator.mjs @@ -14,6 +14,7 @@ const SCANNER = 'SET'; /** Known top-level settings.json keys (as of April 2026) */ const KNOWN_KEYS = new Set([ + 'additionalDirectories', 'agent', 'allowedChannelPlugins', 'allowedHttpHookUrls', 'allowedMcpServers', 'allowManagedHooksOnly', 'allowManagedMcpServersOnly', 'allowManagedPermissionRulesOnly', 'alwaysThinkingEnabled', 'apiKeyHelper', 'attribution', 'autoMemoryDirectory', @@ -64,6 +65,10 @@ const TYPE_CHECKS = new Map([ /** Valid effortLevel values */ const VALID_EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'max']); +/** v5 M6: warn when additionalDirectories grows beyond this — each entry adds + * a project root to walks/discovery, inflating per-turn cost and confusing scope. */ +const ADDITIONAL_DIRS_THRESHOLD = 2; + /** * Scan all settings.json files discovered. * @param {string} targetPath @@ -203,6 +208,26 @@ export async function scan(targetPath, discovery) { } } + // additionalDirectories threshold (v5 M6) + if (Array.isArray(parsed.additionalDirectories) && + parsed.additionalDirectories.length > ADDITIONAL_DIRS_THRESHOLD) { + findings.push(finding({ + scanner: SCANNER, + severity: SEVERITY.low, + title: 'Many additionalDirectories entries', + description: + `${file.relPath}: additionalDirectories has ${parsed.additionalDirectories.length} ` + + `entries (>${ADDITIONAL_DIRS_THRESHOLD}). Each entry expands Claude's read scope ` + + 'across additional project roots, inflating discovery cost and risking unintended access.', + file: file.absPath, + evidence: parsed.additionalDirectories.slice(0, 5).map(d => `"${d}"`).join(', '), + recommendation: + 'Trim to the minimum set needed. Prefer launching Claude from the relevant root ' + + 'rather than chaining many directories.', + autoFixable: false, + })); + } + // hooks checks (basic — detailed in hook-validator) if (parsed.hooks) { if (Array.isArray(parsed.hooks)) { diff --git a/plugins/config-audit/tests/fixtures/additional-dirs-many/.claude/settings.json b/plugins/config-audit/tests/fixtures/additional-dirs-many/.claude/settings.json new file mode 100644 index 0000000..4b1471d --- /dev/null +++ b/plugins/config-audit/tests/fixtures/additional-dirs-many/.claude/settings.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "additionalDirectories": [ + "~/work/repo-a", + "~/work/repo-b", + "~/work/repo-c" + ] +} diff --git a/plugins/config-audit/tests/fixtures/additional-dirs-ok/.claude/settings.json b/plugins/config-audit/tests/fixtures/additional-dirs-ok/.claude/settings.json new file mode 100644 index 0000000..27b058d --- /dev/null +++ b/plugins/config-audit/tests/fixtures/additional-dirs-ok/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "additionalDirectories": [ + "~/work/repo-a", + "~/work/repo-b" + ] +} diff --git a/plugins/config-audit/tests/scanners/settings-validator.test.mjs b/plugins/config-audit/tests/scanners/settings-validator.test.mjs index 1833106..2c2d611 100644 --- a/plugins/config-audit/tests/scanners/settings-validator.test.mjs +++ b/plugins/config-audit/tests/scanners/settings-validator.test.mjs @@ -76,6 +76,39 @@ describe('SET scanner — broken project', () => { }); }); +describe('SET scanner — additionalDirectories (v5 M6)', () => { + it('does NOT flag additionalDirectories as unknown key', async () => { + resetCounter(); + const path = resolve(FIXTURES, 'additional-dirs-ok'); + const discovery = await discoverConfigFiles(path); + const result = await scan(path, discovery); + const unknown = result.findings.find(f => + f.title === 'Unknown settings key' && /additionalDirectories/.test(f.evidence || '')); + assert.equal(unknown, undefined, + 'additionalDirectories should be in KNOWN_KEYS'); + }); + + it('does NOT flag 2 entries as too many', async () => { + resetCounter(); + const path = resolve(FIXTURES, 'additional-dirs-ok'); + const discovery = await discoverConfigFiles(path); + const result = await scan(path, discovery); + const f = result.findings.find(x => /additionalDirectories/i.test(x.title || '')); + assert.equal(f, undefined, + `expected no additionalDirectories threshold finding for 2 entries, got: ${f?.title}`); + }); + + it('flags > 2 entries as low finding', async () => { + resetCounter(); + const path = resolve(FIXTURES, 'additional-dirs-many'); + const discovery = await discoverConfigFiles(path); + const result = await scan(path, discovery); + const f = result.findings.find(x => /additionalDirectories/i.test(x.title || '')); + assert.ok(f, `expected additionalDirectories threshold finding; got: ${result.findings.map(x => x.title).join(' | ')}`); + assert.equal(f.severity, 'low', `expected low severity, got ${f.severity}`); + }); +}); + describe('SET scanner — empty project', () => { let result; beforeEach(async () => {