feat(set): validate autoMode structure + flag it in shared settings (CA-SET)
settings-validator now validates the autoMode block (auto-mode classifier
config). Structure (medium): autoMode must be an object whose only keys are
environment/allow/soft_deny/hard_deny, each a string array ("$defaults" is a
valid entry); flags not-an-object, unknown-subkey, not-string-array. Dead-config
(low): Claude Code does not read autoMode from shared project settings
(.claude/settings.json), so an autoMode block committed there has no effect —
keyed on file.scope === 'project'.
Both premises primary-source-verified (code.claude.com/docs/en/auto-mode-config).
The plan's "test per-file scope first" gate passed: ConfigFile already carries
scope. SET is in the orchestrator; SC-5 re-checked, byte-equal (snapshot fixture
has no autoMode). Fixtures force-added (.claude/ is gitignored).
Tests +5 (944->949). Scanner count unchanged (13). --json/--raw byte-stable.
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:
parent
9fd14aee99
commit
3633571c7e
8 changed files with 225 additions and 2 deletions
|
|
@ -75,6 +75,11 @@ 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;
|
||||
|
||||
/** 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. */
|
||||
const AUTO_MODE_SUBKEYS = new Set(['environment', 'allow', 'soft_deny', 'hard_deny']);
|
||||
|
||||
/**
|
||||
* Scan all settings.json files discovered.
|
||||
* @param {string} targetPath
|
||||
|
|
@ -234,6 +239,75 @@ export async function scan(targetPath, discovery) {
|
|||
}));
|
||||
}
|
||||
|
||||
// autoMode validation (auto-mode classifier config).
|
||||
// Structure: must be an object whose only keys are the four documented
|
||||
// prose-rule arrays (each an array of strings; "$defaults" is a valid entry).
|
||||
// Dead-config: Claude Code does NOT read autoMode from SHARED project settings
|
||||
// (.claude/settings.json) — "a checked-in repo cannot inject its own allow
|
||||
// rules" — so it is only honored in user/local/managed scopes.
|
||||
if (parsed.autoMode !== undefined) {
|
||||
const am = parsed.autoMode;
|
||||
if (typeof am !== 'object' || am === null || Array.isArray(am)) {
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity: SEVERITY.medium,
|
||||
title: 'autoMode must be an object',
|
||||
description: `${file.relPath}: "autoMode" must be an object with environment/allow/soft_deny/hard_deny arrays, got ${Array.isArray(am) ? 'array' : typeof am}.`,
|
||||
file: file.absPath,
|
||||
evidence: `autoMode: ${JSON.stringify(am)}`,
|
||||
recommendation: 'Set autoMode to an object, e.g. { "environment": ["$defaults"] }.',
|
||||
autoFixable: false,
|
||||
details: { key: 'autoMode', problem: 'not-an-object' },
|
||||
}));
|
||||
} else {
|
||||
for (const subKey of Object.keys(am)) {
|
||||
if (!AUTO_MODE_SUBKEYS.has(subKey)) {
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity: SEVERITY.medium,
|
||||
title: `autoMode has an unknown sub-key: ${subKey}`,
|
||||
description: `${file.relPath}: "autoMode.${subKey}" is not a recognized sub-key. Valid keys are environment, allow, soft_deny, hard_deny. It is silently ignored — a typo of a real key (e.g. "hard_denies") means those rules never apply.`,
|
||||
file: file.absPath,
|
||||
evidence: `autoMode.${subKey}`,
|
||||
recommendation: 'Use only environment, allow, soft_deny, hard_deny. Check for typos.',
|
||||
autoFixable: false,
|
||||
details: { key: 'autoMode', subKey, problem: 'unknown-subkey' },
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
const val = am[subKey];
|
||||
const isStringArray = Array.isArray(val) && val.every(e => typeof e === 'string');
|
||||
if (!isStringArray) {
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity: SEVERITY.medium,
|
||||
title: `autoMode.${subKey} must be an array of strings`,
|
||||
description: `${file.relPath}: "autoMode.${subKey}" must be an array of prose-rule strings (the literal "$defaults" is allowed), got ${Array.isArray(val) ? 'an array with a non-string entry' : typeof val}.`,
|
||||
file: file.absPath,
|
||||
evidence: `autoMode.${subKey}: ${JSON.stringify(val)}`,
|
||||
recommendation: `Set "autoMode.${subKey}" to an array of strings, e.g. ["$defaults"].`,
|
||||
autoFixable: false,
|
||||
details: { key: 'autoMode', subKey, problem: 'not-string-array' },
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Dead-config: shared project settings (.claude/settings.json) is not read.
|
||||
if (file.scope === 'project') {
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity: SEVERITY.low,
|
||||
title: 'autoMode in shared project settings is ignored by Claude Code',
|
||||
description: `${file.relPath}: Claude Code does not read "autoMode" from shared project settings (.claude/settings.json), so a checked-in repo cannot inject its own rules. This autoMode block has no effect where it is.`,
|
||||
file: file.absPath,
|
||||
evidence: 'autoMode in shared .claude/settings.json (project scope)',
|
||||
recommendation: 'Move autoMode to user settings (~/.claude/settings.json), local settings (.claude/settings.local.json), or managed settings.',
|
||||
autoFixable: false,
|
||||
details: { key: 'autoMode', problem: 'shared-project-scope' },
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// hooks checks (basic — detailed in hook-validator)
|
||||
if (parsed.hooks) {
|
||||
if (Array.isArray(parsed.hooks)) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue