fix(acr): SET typo-gates unknown-key false positives (M-BUG-10)

The CC settings schema is passthrough (verified against the 2.1.193 binary): it
forwards unrecognized keys unchanged rather than rejecting them, so an arbitrary
unknown key is valid/forward-compatible, not an error — the finding's "silently
ignored" claim was factually wrong. The only real risk is a TYPO of a real key
(the intended setting then silently has no effect). Fix: flag an unknown key only
when it closely matches a known key (new levenshtein helper; edit distance <= 2,
both keys >= 4 chars); severity medium -> low; honest passthrough framing in the
scanner + humanizer. Also refreshed KNOWN_KEYS with 6 binary-verified keys
(agentPushNotifEnabled, remoteControlAtStartup, skipAutoPermissionPrompt,
skipDangerousModePermissionPrompt, skipWorkflowUsageWarning, tui). Suite 1341/0
(+12). Frozen v5.0.0 snapshots untouched (0 CA-SET findings there), no re-seed.
Dogfood ~/.claude/settings.json 6->0 (all 6 keys above were false unknown-key
findings; 0 typo flags introduced across 167 walked files).
This commit is contained in:
Kjell Tore Guttormsen 2026-06-26 15:15:14 +02:00
commit 3cf5c714a2
5 changed files with 174 additions and 16 deletions

View file

@ -8,11 +8,11 @@ import { readTextFile } from './lib/file-discovery.mjs';
import { finding, scannerResult } from './lib/output.mjs';
import { SEVERITY } from './lib/severity.mjs';
import { parseJson } from './lib/yaml-parser.mjs';
import { extractKeys } from './lib/string-utils.mjs';
import { extractKeys, levenshtein } from './lib/string-utils.mjs';
const SCANNER = 'SET';
/** Known top-level settings.json keys (as of CC 2.1.181 / June 2026) */
/** Known top-level settings.json keys (as of CC 2.1.193 / June 2026) */
const KNOWN_KEYS = new Set([
'additionalDirectories',
'agent', 'allowAllClaudeAiMcps', 'allowedChannelPlugins', 'allowedHttpHookUrls',
@ -37,6 +37,9 @@ const KNOWN_KEYS = new Set([
'spinnerTipsOverride', 'spinnerVerbs', 'statusLine', 'strictKnownMarketplaces',
'useAutoModeDuringPlan', 'voiceEnabled', 'wheelScrollAccelerationEnabled',
'worktree', '$schema',
// CC 2.1.193 binary-verified (M-BUG-10): present as quoted string literals in the binary
'agentPushNotifEnabled', 'remoteControlAtStartup', 'skipAutoPermissionPrompt',
'skipDangerousModePermissionPrompt', 'skipWorkflowUsageWarning', 'tui',
]);
/** Deprecated keys with migration info */
@ -75,6 +78,16 @@ const VALID_EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
* a project root to walks/discovery, inflating per-turn cost and confusing scope. */
const ADDITIONAL_DIRS_THRESHOLD = 2;
/** M-BUG-10: the CC settings schema is passthrough it forwards unrecognized
* keys unchanged rather than rejecting them, so an arbitrary unknown key is
* valid/forward-compatible, not an error. The only real risk is a TYPO of a
* real key (the intended setting silently does nothing), so an unknown key is
* flagged ONLY when it closely matches a known key: edit distance within
* TYPO_MAX_DISTANCE and both keys at least TYPO_MIN_LEN chars (short keys are
* too noisy for reliable edit-distance matching). */
const TYPO_MAX_DISTANCE = 2;
const TYPO_MIN_LEN = 4;
/** The only valid sub-keys of `autoMode`, each a prose-rule string array
* (the literal "$defaults" is a valid entry). Verified against
* code.claude.com/docs/en/auto-mode-config. */
@ -115,17 +128,32 @@ export async function scan(targetPath, discovery) {
continue;
}
// Check for unknown keys
// Check for unknown keys — typo gate (M-BUG-10). The CC settings schema is
// passthrough, so an unrecognized key is NOT an error; only a typo of a real
// key is (the intended setting silently does nothing). Flag a key only when
// it closely matches a known key; an unknown key far from every known key is
// treated as valid/forward-compatible and emitted nothing.
for (const key of Object.keys(parsed)) {
if (!KNOWN_KEYS.has(key)) {
if (KNOWN_KEYS.has(key)) continue;
let nearest = null;
let best = Infinity;
for (const known of KNOWN_KEYS) {
if (Math.min(key.length, known.length) < TYPO_MIN_LEN) continue;
const d = levenshtein(key, known);
if (d <= TYPO_MAX_DISTANCE && d < best) {
best = d;
nearest = known;
}
}
if (nearest) {
findings.push(finding({
scanner: SCANNER,
severity: SEVERITY.medium,
title: 'Unknown settings key',
description: `${file.relPath}: "${key}" is not a recognized settings.json key. It will be silently ignored.`,
severity: SEVERITY.low,
title: 'Possible typo in settings key',
description: `${file.relPath}: "${key}" is not a recognized settings.json key, but it closely matches "${nearest}". Claude Code forwards unrecognized keys unchanged (it does not reject them), so if "${key}" is a typo of "${nearest}" the intended setting silently has no effect.`,
file: file.absPath,
evidence: key,
recommendation: 'Check spelling. See https://json.schemastore.org/claude-code-settings.json for valid keys.',
recommendation: `Did you mean "${nearest}"? Fix the spelling, or keep "${key}" if it is intentional (e.g. a newer settings key this audit does not recognize yet).`,
autoFixable: false,
}));
}