From b6a62d7699cb9e6708d8c2b51e998dc941078812 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Sat, 20 Jun 2026 09:56:00 +0200 Subject: [PATCH] fix(hkv,rul): add 3 verified hook events; correct globs-rule wording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HKV: add Setup, UserPromptExpansion, PostToolBatch to VALID_EVENTS, verified live against code.claude.com/docs/en/hooks.md (2026-06-19). A valid hook using one of these was wrongly flagged "will never fire" — a user could delete a working hook. Made the "(N total)" hint dynamic so it can't drift again. Flagged the unverified kebab 'post-session' in a comment (an existing test depends on it; follow-up check needed). RUL: reword the globs finding. Only `paths:` is documented; whether CC ever read `globs` is unverified, so the old "deprecated/legacy" framing overclaimed (Verifiseringsplikt). New wording steers to the documented `paths:` field. Updated the coupled fix-engine title match and the humanizer entry (which also carried the "field was renamed" overclaim). Suite 950 -> 954 (badge bumped). self-audit A/A, scanner count 13. No version bump — these land in the pending v5.4.1 patch. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8 --- README.md | 2 +- scanners/fix-engine.mjs | 2 +- scanners/hook-validator.mjs | 17 +++++---- scanners/lib/humanizer-data.mjs | 8 ++-- scanners/rules-validator.mjs | 6 +-- tests/scanners/hook-validator.test.mjs | 50 +++++++++++++++++++++++++ tests/scanners/rules-validator.test.mjs | 18 +++++++-- 7 files changed, 84 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 12a777f..9d78c0c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ![Commands](https://img.shields.io/badge/commands-18-green) ![Agents](https://img.shields.io/badge/agents-6-orange) ![Hooks](https://img.shields.io/badge/hooks-4-red) -![Tests](https://img.shields.io/badge/tests-950+-brightgreen) +![Tests](https://img.shields.io/badge/tests-954+-brightgreen) ![License](https://img.shields.io/badge/license-MIT-lightgrey) A Claude Code plugin that checks configuration health, suggests context-aware improvements, and auto-fixes issues — `CLAUDE.md`, `settings.json`, hooks, rules, MCP servers, `@imports`, and plugins. 13 deterministic scanners across 10 quality areas, context-aware feature recommendations, auto-fix with backup/rollback, a prompt-cache-aware Token Hotspots scanner with optional API-calibrated `--accurate-tokens` mode, plus cache-prefix stability, dead-tool, and cross-plugin collision detection. Zero external dependencies. diff --git a/scanners/fix-engine.mjs b/scanners/fix-engine.mjs index a4cda53..c04a6e8 100644 --- a/scanners/fix-engine.mjs +++ b/scanners/fix-engine.mjs @@ -180,7 +180,7 @@ function createFixPlan(finding) { // --- RUL scanner fixes --- if (scanner === 'RUL') { - if (title === 'Rule uses deprecated "globs" field') { + if (title === 'Rule uses "globs" instead of documented "paths"') { return { ...base, type: FIX_TYPES.FRONTMATTER_RENAME, diff --git a/scanners/hook-validator.mjs b/scanners/hook-validator.mjs index 196b4f8..49dba8d 100644 --- a/scanners/hook-validator.mjs +++ b/scanners/hook-validator.mjs @@ -13,11 +13,12 @@ import { resolve, dirname } from 'node:path'; const SCANNER = 'HKV'; -/** All valid hook events (as of June 2026) */ +/** All valid hook events — verified against code.claude.com/docs/en/hooks.md (2026-06-19) */ const VALID_EVENTS = new Set([ - 'SessionStart', 'InstructionsLoaded', 'UserPromptSubmit', + 'SessionStart', 'Setup', 'InstructionsLoaded', + 'UserPromptSubmit', 'UserPromptExpansion', 'PreToolUse', 'PermissionRequest', 'PermissionDenied', - 'PostToolUse', 'PostToolUseFailure', + 'PostToolUse', 'PostToolUseFailure', 'PostToolBatch', 'SubagentStart', 'SubagentStop', 'TaskCreated', 'TaskCompleted', 'Stop', 'StopFailure', @@ -26,9 +27,11 @@ const VALID_EVENTS = new Set([ 'WorktreeCreate', 'WorktreeRemove', 'PreCompact', 'PostCompact', 'Elicitation', 'ElicitationResult', - 'SessionEnd', - // CC 2.1.152 (MessageDisplay), 2.1.169 (post-session, kebab — distinct from SessionEnd) - 'MessageDisplay', 'post-session', + 'SessionEnd', 'MessageDisplay', + // NOTE: 'post-session' (kebab) is UNVERIFIED against current docs — kept + // pending confirmation (an existing test depends on it; hook events are + // otherwise PascalCase, so this is flagged for a follow-up check). + 'post-session', ]); /** Valid hook handler types */ @@ -136,7 +139,7 @@ async function validateHooksObject(hooks, file, findings, baseDir) { description: `${file.relPath}: "${event}" is not a valid hook event. This hook will never fire.`, file: file.absPath, evidence: event, - recommendation: `Valid events: ${[...VALID_EVENTS].slice(0, 8).join(', ')}... (28 total)`, + recommendation: `Valid events: ${[...VALID_EVENTS].slice(0, 8).join(', ')}... (${VALID_EVENTS.size} total)`, autoFixable: false, })); continue; diff --git a/scanners/lib/humanizer-data.mjs b/scanners/lib/humanizer-data.mjs index 1eb64d2..367fe4b 100644 --- a/scanners/lib/humanizer-data.mjs +++ b/scanners/lib/humanizer-data.mjs @@ -254,10 +254,10 @@ export const TRANSLATIONS = { description: 'Without scoping, the rule loads on every conversation regardless of which files you\'re working with.', recommendation: 'Add a scoping block at the top of the file to limit when the rule loads (see the details).', }, - 'Rule uses deprecated "globs" field': { - title: 'A rule uses an old field name', - description: 'The field was renamed; the old name still works for now but may stop working in a future release.', - recommendation: 'Rename the field to the current equivalent shown in the details.', + 'Rule uses "globs" instead of documented "paths"': { + title: 'A rule uses an unrecognized scoping field', + description: 'Claude Code\'s docs use `paths:` to scope a rule; `globs:` is not the documented field, so the rule may not scope the way you intend.', + recommendation: 'Rename the field to `paths:` (see the details).', }, 'Rule file is not .md': { title: 'A rule file uses an unexpected extension', diff --git a/scanners/rules-validator.mjs b/scanners/rules-validator.mjs index 08d4f57..3861066 100644 --- a/scanners/rules-validator.mjs +++ b/scanners/rules-validator.mjs @@ -62,11 +62,11 @@ export async function scan(targetPath, discovery) { findings.push(finding({ scanner: SCANNER, severity: SEVERITY.low, - title: 'Rule uses deprecated "globs" field', - description: `${file.relPath} uses "globs:" which is legacy. Use "paths:" instead.`, + title: 'Rule uses "globs" instead of documented "paths"', + description: `${file.relPath} uses "globs:" for scoping. Claude Code's documentation specifies "paths:" as the rule-scoping field; "globs:" is not documented. Rename to "paths:" so the rule scopes as intended.`, file: file.absPath, evidence: `globs: ${JSON.stringify(frontmatter.globs)}`, - recommendation: 'Rename "globs:" to "paths:" in frontmatter.', + recommendation: 'Rename "globs:" to "paths:" — paths: is the documented field for path-scoped rules.', autoFixable: true, })); } diff --git a/tests/scanners/hook-validator.test.mjs b/tests/scanners/hook-validator.test.mjs index 25b315a..871a36f 100644 --- a/tests/scanners/hook-validator.test.mjs +++ b/tests/scanners/hook-validator.test.mjs @@ -148,6 +148,56 @@ describe('HKV scanner — CC 2.1.152/2.1.169 hook events (Batch 1 false-positive }); }); +describe('HKV scanner — Setup/UserPromptExpansion/PostToolBatch events (Batch 2 false-positive fix)', () => { + // Three more events verified live against code.claude.com/docs/en/hooks.md + // (2026-06-19): Setup (session-level), UserPromptExpansion (per-turn), + // PostToolBatch (agentic loop). Same hermetic temp-fixture pattern — the + // path-guard blocks committing settings.json/hooks.json fixtures. + let tmpRoot; + let result; + + const NEW_EVENTS = ['Setup', 'UserPromptExpansion', 'PostToolBatch']; + + beforeEach(async () => { + resetCounter(); + tmpRoot = await mkdtemp(join(tmpdir(), 'ca-hkv-events2-')); + await mkdir(join(tmpRoot, '.claude'), { recursive: true }); + const settings = { + hooks: { + // 'echo …' commands skip the script-existence check, isolating + // event-name validation. + Setup: [{ hooks: [{ type: 'command', command: 'echo setup' }] }], + UserPromptExpansion: [{ hooks: [{ type: 'command', command: 'echo expand' }] }], + PostToolBatch: [{ hooks: [{ type: 'command', command: 'echo batch' }] }], + }, + }; + 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 }); + }); + + for (const event of NEW_EVENTS) { + it(`does NOT flag "${event}" as an unknown hook event`, () => { + const unknown = result.findings.find(f => + f.title === 'Unknown hook event' && f.evidence === event); + assert.equal(unknown, undefined, `${event} should be in VALID_EVENTS`); + }); + } + + it('produces zero findings for a valid Setup + UserPromptExpansion + PostToolBatch config', () => { + assert.equal(result.findings.length, 0, + `expected clean scan; got: ${result.findings.map(f => `${f.title}:${f.evidence || ''}`).join(' | ')}`); + }); +}); + describe('HKV scanner — empty project', () => { let result; beforeEach(async () => { diff --git a/tests/scanners/rules-validator.test.mjs b/tests/scanners/rules-validator.test.mjs index 8e3949d..ad98bf9 100644 --- a/tests/scanners/rules-validator.test.mjs +++ b/tests/scanners/rules-validator.test.mjs @@ -45,9 +45,21 @@ describe('RUL scanner — broken project', () => { result = await scan(resolve(FIXTURES, 'broken-project'), discovery); }); - it('detects deprecated globs field', () => { - const found = result.findings.some(f => f.title.includes('deprecated')); - assert.ok(found, 'Should detect globs: instead of paths:'); + it('flags a "globs" rule and steers to the documented "paths" field', () => { + const f = result.findings.find(x => /globs/i.test(x.title)); + assert.ok(f, 'Should flag globs: usage'); + // Verifiseringsplikt: only `paths` is documented; whether CC ever read + // `globs` is unverified, so the wording must NOT claim it is + // deprecated/legacy Claude Code syntax. + assert.ok( + !/deprecated|legacy/i.test(`${f.title} ${f.description} ${f.recommendation}`), + 'wording must not assert globs is deprecated/legacy CC syntax (unverified)', + ); + assert.match( + `${f.description} ${f.recommendation}`, + /paths/, + 'should steer to the documented paths: field', + ); }); it('detects dead rule (matches no files)', () => {