From 73099354c738c87c72b63d5bd661c9e03dcc3202 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Thu, 18 Jun 2026 11:58:42 +0200 Subject: [PATCH] =?UTF-8?q?fix(settings-validator):=20accept=20CC=202.1.11?= =?UTF-8?q?4=E2=80=93181=20keys=20+=20xhigh=20effort?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clears a cluster of active false positives where valid, documented Claude Code config was flagged as unknown/invalid (gap matrix, Batch 1). KNOWN_KEYS +11 (CC 2.1.133–181): allowAllClaudeAiMcps, disableBundledSkills, enforceAvailableModels, fallbackModel, footerLinksRegexes, parentSettingsBehavior, pluginSuggestionMarketplaces, requiredMaximumVersion, requiredMinimumVersion, sandbox, wheelScrollAccelerationEnabled. VALID_EFFORT_LEVELS += 'xhigh' (CC 2.1.154 Opus-4.8 top tier). TYPE_CHECKS += disableBundledSkills/wheelScrollAccelerationEnabled (boolean). fallbackModel intentionally NOT type-checked (string | array<=3). Tests: hermetic runtime temp-fixture (path-guard blocks committing settings.json); 29/29 SET green, full hermetic suite unaffected. Ref: docs/cc-2.1.x-gap-matrix.md Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8 --- scanners/settings-validator.mjs | 43 ++++++------ tests/scanners/settings-validator.test.mjs | 76 +++++++++++++++++++++- 2 files changed, 98 insertions(+), 21 deletions(-) diff --git a/scanners/settings-validator.mjs b/scanners/settings-validator.mjs index 405bbe3..e7225d4 100644 --- a/scanners/settings-validator.mjs +++ b/scanners/settings-validator.mjs @@ -12,26 +12,29 @@ import { extractKeys } from './lib/string-utils.mjs'; const SCANNER = 'SET'; -/** Known top-level settings.json keys (as of April 2026) */ +/** Known top-level settings.json keys (as of CC 2.1.181 / June 2026) */ const KNOWN_KEYS = new Set([ 'additionalDirectories', - 'agent', 'allowedChannelPlugins', 'allowedHttpHookUrls', 'allowedMcpServers', - 'allowManagedHooksOnly', 'allowManagedMcpServersOnly', 'allowManagedPermissionRulesOnly', - 'alwaysThinkingEnabled', 'apiKeyHelper', 'attribution', 'autoMemoryDirectory', - 'autoMemoryEnabled', 'autoMode', 'autoUpdatesChannel', 'availableModels', - 'awsAuthRefresh', 'awsCredentialExport', 'blockedMarketplaces', 'channelsEnabled', - 'cleanupPeriodDays', 'claudeMdExcludes', 'companyAnnouncements', 'defaultShell', - 'deniedMcpServers', 'disableAllHooks', 'disableAutoMode', 'disableDeepLinkRegistration', + 'agent', 'allowAllClaudeAiMcps', 'allowedChannelPlugins', 'allowedHttpHookUrls', + 'allowedMcpServers', 'allowManagedHooksOnly', 'allowManagedMcpServersOnly', + 'allowManagedPermissionRulesOnly', 'alwaysThinkingEnabled', 'apiKeyHelper', + 'attribution', 'autoMemoryDirectory', 'autoMemoryEnabled', 'autoMode', + 'autoUpdatesChannel', 'availableModels', 'awsAuthRefresh', 'awsCredentialExport', + 'blockedMarketplaces', 'channelsEnabled', 'cleanupPeriodDays', 'claudeMdExcludes', + 'companyAnnouncements', 'defaultShell', 'deniedMcpServers', 'disableAllHooks', + 'disableAutoMode', 'disableBundledSkills', 'disableDeepLinkRegistration', 'disabledMcpjsonServers', 'effortLevel', 'enableAllProjectMcpServers', - 'enabledMcpjsonServers', 'enabledPlugins', 'env', 'extraKnownMarketplaces', - 'fastModePerSessionOptIn', 'feedbackSurveyRate', 'fileSuggestion', - 'forceLoginMethod', 'forceLoginOrgUUID', 'hooks', 'httpHookAllowedEnvVars', - 'includeCoAuthoredBy', 'includeGitInstructions', 'language', 'model', - 'modelOverrides', 'otelHeadersHelper', 'outputStyle', 'permissions', - 'plansDirectory', 'pluginTrustMessage', 'prefersReducedMotion', - 'respectGitignore', 'showClearContextOnPlanAccept', 'showThinkingSummaries', - 'spinnerTipsEnabled', 'spinnerTipsOverride', 'spinnerVerbs', 'statusLine', - 'strictKnownMarketplaces', 'useAutoModeDuringPlan', 'voiceEnabled', + 'enabledMcpjsonServers', 'enabledPlugins', 'enforceAvailableModels', 'env', + 'extraKnownMarketplaces', 'fallbackModel', 'fastModePerSessionOptIn', + 'feedbackSurveyRate', 'fileSuggestion', 'footerLinksRegexes', 'forceLoginMethod', + 'forceLoginOrgUUID', 'hooks', 'httpHookAllowedEnvVars', 'includeCoAuthoredBy', + 'includeGitInstructions', 'language', 'model', 'modelOverrides', 'otelHeadersHelper', + 'outputStyle', 'parentSettingsBehavior', 'permissions', 'plansDirectory', + 'pluginSuggestionMarketplaces', 'pluginTrustMessage', 'prefersReducedMotion', + 'requiredMaximumVersion', 'requiredMinimumVersion', 'respectGitignore', 'sandbox', + 'showClearContextOnPlanAccept', 'showThinkingSummaries', 'spinnerTipsEnabled', + 'spinnerTipsOverride', 'spinnerVerbs', 'statusLine', 'strictKnownMarketplaces', + 'useAutoModeDuringPlan', 'voiceEnabled', 'wheelScrollAccelerationEnabled', 'worktree', '$schema', ]); @@ -47,6 +50,7 @@ const TYPE_CHECKS = new Map([ ['channelsEnabled', 'boolean'], ['cleanupPeriodDays', 'number'], ['disableAllHooks', 'boolean'], + ['disableBundledSkills', 'boolean'], ['effortLevel', 'string'], ['enableAllProjectMcpServers', 'boolean'], ['fastModePerSessionOptIn', 'boolean'], @@ -60,10 +64,11 @@ const TYPE_CHECKS = new Map([ ['showThinkingSummaries', 'boolean'], ['spinnerTipsEnabled', 'boolean'], ['voiceEnabled', 'boolean'], + ['wheelScrollAccelerationEnabled', 'boolean'], ]); -/** Valid effortLevel values */ -const VALID_EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'max']); +/** Valid effortLevel values (CC 2.1.154 added 'xhigh' as the Opus-4.8 top tier) */ +const VALID_EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'xhigh', '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. */ diff --git a/tests/scanners/settings-validator.test.mjs b/tests/scanners/settings-validator.test.mjs index 3084328..85f90cf 100644 --- a/tests/scanners/settings-validator.test.mjs +++ b/tests/scanners/settings-validator.test.mjs @@ -1,7 +1,9 @@ -import { describe, it, beforeEach } from 'node:test'; +import { describe, it, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert/strict'; -import { resolve } from 'node:path'; +import { resolve, join } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; import { resetCounter } from '../../scanners/lib/output.mjs'; import { discoverConfigFiles } from '../../scanners/lib/file-discovery.mjs'; import { scan } from '../../scanners/settings-validator.mjs'; @@ -119,6 +121,76 @@ describe('SET scanner — additionalDirectories (v5 M6)', () => { }); }); +describe('SET scanner — CC 2.1.114→181 valid keys (Batch 1 false-positive fix)', () => { + // The pre-write path-guard blocks the agent from committing settings.json + // fixtures, so this suite materializes a hermetic temp fixture at runtime. + let tmpRoot; + let result; + + const NEW_KEYS = [ + 'sandbox', 'fallbackModel', 'enforceAvailableModels', 'disableBundledSkills', + 'pluginSuggestionMarketplaces', 'requiredMinimumVersion', 'requiredMaximumVersion', + 'allowAllClaudeAiMcps', 'footerLinksRegexes', 'wheelScrollAccelerationEnabled', + 'parentSettingsBehavior', + ]; + + beforeEach(async () => { + resetCounter(); + tmpRoot = await mkdtemp(join(tmpdir(), 'ca-set-2181-')); + await mkdir(join(tmpRoot, '.claude'), { recursive: true }); + const settings = { + $schema: 'https://json.schemastore.org/claude-code-settings.json', + effortLevel: 'xhigh', // CC 2.1.154 + sandbox: { allowAppleEvents: false }, // CC 2.1.181 + fallbackModel: ['claude-opus-4-8', 'claude-sonnet-4-6'], // CC 2.1.166 (string|array) + enforceAvailableModels: true, // CC 2.1.175 + disableBundledSkills: true, // CC 2.1.169 + pluginSuggestionMarketplaces: ['acme'], // CC 2.1.152 + requiredMinimumVersion: '2.1.114', // CC 2.1.163 + requiredMaximumVersion: '2.2.0', // CC 2.1.163 + allowAllClaudeAiMcps: false, // CC 2.1.149 + footerLinksRegexes: ['^https://internal\\.'], // CC 2.1.181 + wheelScrollAccelerationEnabled: true, // CC 2.1.174 + parentSettingsBehavior: 'merge', // CC 2.1.133 + permissions: { deny: ['Read(./.env)'], allow: ['Bash(npm run *)'] }, + }; + await writeFile( + join(tmpRoot, '.claude', 'settings.json'), + JSON.stringify(settings, null, 2) + '\n', + 'utf8', + ); + const discovery = await discoverConfigFiles(tmpRoot); + result = await scan(tmpRoot, discovery); + }); + + afterEach(async () => { + if (tmpRoot) await rm(tmpRoot, { recursive: true, force: true }); + }); + + it('accepts effortLevel "xhigh" (CC 2.1.154)', () => { + const bad = result.findings.find(f => /xhigh/.test(f.evidence || '')); + assert.equal(bad, undefined, 'xhigh must be a valid effortLevel'); + }); + + for (const key of NEW_KEYS) { + it(`does NOT flag "${key}" as an unknown key`, () => { + const unknown = result.findings.find(f => + f.title === 'Unknown settings key' && f.evidence === key); + assert.equal(unknown, undefined, `${key} should be in KNOWN_KEYS`); + }); + } + + it('treats fallbackModel array value without a single-type mismatch', () => { + const f = result.findings.find(x => /fallbackModel/.test(x.evidence || '')); + assert.equal(f, undefined, 'fallbackModel (string|array) must not be type-checked'); + }); + + it('produces zero findings for an all-valid CC 2.1.181 settings file', () => { + assert.equal(result.findings.length, 0, + `expected clean scan; got: ${result.findings.map(f => `${f.title}:${f.evidence || ''}`).join(' | ')}`); + }); +}); + describe('SET scanner — empty project', () => { let result; beforeEach(async () => {