fix(settings-validator): accept CC 2.1.114–181 keys + xhigh effort

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
This commit is contained in:
Kjell Tore Guttormsen 2026-06-18 11:58:42 +02:00
commit 73099354c7
2 changed files with 98 additions and 21 deletions

View file

@ -12,26 +12,29 @@ import { extractKeys } from './lib/string-utils.mjs';
const SCANNER = 'SET'; 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([ const KNOWN_KEYS = new Set([
'additionalDirectories', 'additionalDirectories',
'agent', 'allowedChannelPlugins', 'allowedHttpHookUrls', 'allowedMcpServers', 'agent', 'allowAllClaudeAiMcps', 'allowedChannelPlugins', 'allowedHttpHookUrls',
'allowManagedHooksOnly', 'allowManagedMcpServersOnly', 'allowManagedPermissionRulesOnly', 'allowedMcpServers', 'allowManagedHooksOnly', 'allowManagedMcpServersOnly',
'alwaysThinkingEnabled', 'apiKeyHelper', 'attribution', 'autoMemoryDirectory', 'allowManagedPermissionRulesOnly', 'alwaysThinkingEnabled', 'apiKeyHelper',
'autoMemoryEnabled', 'autoMode', 'autoUpdatesChannel', 'availableModels', 'attribution', 'autoMemoryDirectory', 'autoMemoryEnabled', 'autoMode',
'awsAuthRefresh', 'awsCredentialExport', 'blockedMarketplaces', 'channelsEnabled', 'autoUpdatesChannel', 'availableModels', 'awsAuthRefresh', 'awsCredentialExport',
'cleanupPeriodDays', 'claudeMdExcludes', 'companyAnnouncements', 'defaultShell', 'blockedMarketplaces', 'channelsEnabled', 'cleanupPeriodDays', 'claudeMdExcludes',
'deniedMcpServers', 'disableAllHooks', 'disableAutoMode', 'disableDeepLinkRegistration', 'companyAnnouncements', 'defaultShell', 'deniedMcpServers', 'disableAllHooks',
'disableAutoMode', 'disableBundledSkills', 'disableDeepLinkRegistration',
'disabledMcpjsonServers', 'effortLevel', 'enableAllProjectMcpServers', 'disabledMcpjsonServers', 'effortLevel', 'enableAllProjectMcpServers',
'enabledMcpjsonServers', 'enabledPlugins', 'env', 'extraKnownMarketplaces', 'enabledMcpjsonServers', 'enabledPlugins', 'enforceAvailableModels', 'env',
'fastModePerSessionOptIn', 'feedbackSurveyRate', 'fileSuggestion', 'extraKnownMarketplaces', 'fallbackModel', 'fastModePerSessionOptIn',
'forceLoginMethod', 'forceLoginOrgUUID', 'hooks', 'httpHookAllowedEnvVars', 'feedbackSurveyRate', 'fileSuggestion', 'footerLinksRegexes', 'forceLoginMethod',
'includeCoAuthoredBy', 'includeGitInstructions', 'language', 'model', 'forceLoginOrgUUID', 'hooks', 'httpHookAllowedEnvVars', 'includeCoAuthoredBy',
'modelOverrides', 'otelHeadersHelper', 'outputStyle', 'permissions', 'includeGitInstructions', 'language', 'model', 'modelOverrides', 'otelHeadersHelper',
'plansDirectory', 'pluginTrustMessage', 'prefersReducedMotion', 'outputStyle', 'parentSettingsBehavior', 'permissions', 'plansDirectory',
'respectGitignore', 'showClearContextOnPlanAccept', 'showThinkingSummaries', 'pluginSuggestionMarketplaces', 'pluginTrustMessage', 'prefersReducedMotion',
'spinnerTipsEnabled', 'spinnerTipsOverride', 'spinnerVerbs', 'statusLine', 'requiredMaximumVersion', 'requiredMinimumVersion', 'respectGitignore', 'sandbox',
'strictKnownMarketplaces', 'useAutoModeDuringPlan', 'voiceEnabled', 'showClearContextOnPlanAccept', 'showThinkingSummaries', 'spinnerTipsEnabled',
'spinnerTipsOverride', 'spinnerVerbs', 'statusLine', 'strictKnownMarketplaces',
'useAutoModeDuringPlan', 'voiceEnabled', 'wheelScrollAccelerationEnabled',
'worktree', '$schema', 'worktree', '$schema',
]); ]);
@ -47,6 +50,7 @@ const TYPE_CHECKS = new Map([
['channelsEnabled', 'boolean'], ['channelsEnabled', 'boolean'],
['cleanupPeriodDays', 'number'], ['cleanupPeriodDays', 'number'],
['disableAllHooks', 'boolean'], ['disableAllHooks', 'boolean'],
['disableBundledSkills', 'boolean'],
['effortLevel', 'string'], ['effortLevel', 'string'],
['enableAllProjectMcpServers', 'boolean'], ['enableAllProjectMcpServers', 'boolean'],
['fastModePerSessionOptIn', 'boolean'], ['fastModePerSessionOptIn', 'boolean'],
@ -60,10 +64,11 @@ const TYPE_CHECKS = new Map([
['showThinkingSummaries', 'boolean'], ['showThinkingSummaries', 'boolean'],
['spinnerTipsEnabled', 'boolean'], ['spinnerTipsEnabled', 'boolean'],
['voiceEnabled', 'boolean'], ['voiceEnabled', 'boolean'],
['wheelScrollAccelerationEnabled', 'boolean'],
]); ]);
/** Valid effortLevel values */ /** 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', 'max']); const VALID_EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
/** v5 M6: warn when additionalDirectories grows beyond this each entry adds /** v5 M6: warn when additionalDirectories grows beyond this each entry adds
* a project root to walks/discovery, inflating per-turn cost and confusing scope. */ * a project root to walks/discovery, inflating per-turn cost and confusing scope. */

View file

@ -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 assert from 'node:assert/strict';
import { resolve } from 'node:path'; import { resolve, join } from 'node:path';
import { fileURLToPath } from 'node:url'; 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 { resetCounter } from '../../scanners/lib/output.mjs';
import { discoverConfigFiles } from '../../scanners/lib/file-discovery.mjs'; import { discoverConfigFiles } from '../../scanners/lib/file-discovery.mjs';
import { scan } from '../../scanners/settings-validator.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', () => { describe('SET scanner — empty project', () => {
let result; let result;
beforeEach(async () => { beforeEach(async () => {