diff --git a/README.md b/README.md index b88934f..4e6457a 100644 --- a/README.md +++ b/README.md @@ -525,7 +525,7 @@ Reference documents that inform the feature-gap agent and context-aware recommen | `claude-code-capabilities.md` | Feature register: 18 config surfaces, Anthropic guidance, relevance table | | `configuration-best-practices.md` | Per-layer best practices (cache-stability guidance) | | `anti-patterns.md` | Common mistakes mapped to scanner IDs | -| `hook-events-reference.md` | All 26 hook events with details | +| `hook-events-reference.md` | All 28 hook events with details | | `feature-evolution.md` | Feature timeline for staleness detection | | `gap-closure-templates.md` | Config-specific templates for closing gaps | | `prompt-cache-patterns.md` | Token-cost dynamics (prompt-cache patterns) — patterns powering the TOK scanner | diff --git a/docs/scanner-internals.md b/docs/scanner-internals.md index e634dd0..fe19728 100644 --- a/docs/scanner-internals.md +++ b/docs/scanner-internals.md @@ -70,7 +70,7 @@ Scanner CLI: `node scanners/scan-orchestrator.mjs [--global] [--full-mach | `claude-code-capabilities.md` | Feature register: 18 config surfaces, Anthropic guidance, relevance table | | `configuration-best-practices.md` | Per-layer best practices (v5: cache-stability guidance replaces Sonnet-era 200-line rule) | | `anti-patterns.md` | Common mistakes mapped to scanner IDs | -| `hook-events-reference.md` | All 26 hook events with details | +| `hook-events-reference.md` | All 28 hook events with details | | `feature-evolution.md` | Feature timeline for staleness detection | | `gap-closure-templates.md` | Config-specific templates for closing gaps | | `prompt-cache-patterns.md` | Token-cost dynamics (prompt-cache patterns) — patterns powering the TOK scanner | diff --git a/scanners/hook-validator.mjs b/scanners/hook-validator.mjs index f38335d..196b4f8 100644 --- a/scanners/hook-validator.mjs +++ b/scanners/hook-validator.mjs @@ -13,7 +13,7 @@ import { resolve, dirname } from 'node:path'; const SCANNER = 'HKV'; -/** All valid hook events (as of April 2026) */ +/** All valid hook events (as of June 2026) */ const VALID_EVENTS = new Set([ 'SessionStart', 'InstructionsLoaded', 'UserPromptSubmit', 'PreToolUse', 'PermissionRequest', 'PermissionDenied', diff --git a/tests/scanners/fix-cli.test.mjs b/tests/scanners/fix-cli.test.mjs index b7474f8..68c36d1 100644 --- a/tests/scanners/fix-cli.test.mjs +++ b/tests/scanners/fix-cli.test.mjs @@ -4,8 +4,9 @@ import { resolve, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { cp, rm, readFile, stat } from 'node:fs/promises'; import { mkdirSync, existsSync, readdirSync } from 'node:fs'; -import { tmpdir, homedir } from 'node:os'; +import { tmpdir } from 'node:os'; import { execFileSync } from 'node:child_process'; +import { hermeticEnv, HERMETIC_HOME } from '../helpers/hermetic-home.mjs'; const __dirname = fileURLToPath(new URL('.', import.meta.url)); const FIXTURES = resolve(__dirname, '../fixtures'); @@ -25,18 +26,31 @@ describe('fix-cli dry-run', () => { const result = execFileSync('node', [FIX_CLI, FIXABLE, '--json'], { encoding: 'utf-8', timeout: 30000, + env: hermeticEnv(), }); const output = JSON.parse(result); assert.ok(Array.isArray(output.planned), 'Should have planned array'); assert.ok(output.planned.length > 0, 'Should have planned fixes'); assert.strictEqual(output.backupId, null, 'No backup in dry-run'); assert.ok(Array.isArray(output.manual), 'Should have manual array'); + + // Regression lock (HOME-leak class): a project-scoped fix run must surface only + // project-local findings, never HOME-scoped SKL/COL findings read from the + // developer's real ~/.claude. Without a hermetic HOME the spawned CLI picks up + // CA-SKL-001 from installed skills, making this test machine-dependent. + const homeScoped = output.manual.filter((m) => /^CA-(SKL|COL)-/.test(m.findingId)); + assert.deepStrictEqual( + homeScoped.map((m) => m.findingId), + [], + 'fix-cli leaked HOME-scoped findings — spawn is not HOME-isolated', + ); }); it('outputs valid JSON with --json flag', () => { const result = execFileSync('node', [FIX_CLI, FIXABLE, '--json'], { encoding: 'utf-8', timeout: 30000, + env: hermeticEnv(), }); assert.doesNotThrow(() => JSON.parse(result), 'Output should be valid JSON'); }); @@ -57,13 +71,15 @@ describe('fix-cli --apply', () => { const result = execFileSync('node', [FIX_CLI, tmpDir, '--apply', '--json'], { encoding: 'utf-8', timeout: 30000, + env: hermeticEnv(), }); const output = JSON.parse(result); assert.ok(output.applied.length > 0, 'Should have applied fixes'); assert.ok(output.backupId, 'Should have a backup ID'); - // Verify backup exists - const backupDir = join(homedir(), '.config-audit', 'backups', output.backupId); + // Verify backup exists. The CLI runs with a hermetic HOME (see execFileSync env + // below), so its backups land under HERMETIC_HOME, not the developer's real home. + const backupDir = join(HERMETIC_HOME, '.config-audit', 'backups', output.backupId); assert.ok(existsSync(backupDir), 'Backup directory should exist'); }); @@ -71,6 +87,7 @@ describe('fix-cli --apply', () => { execFileSync('node', [FIX_CLI, tmpDir, '--apply'], { encoding: 'utf-8', timeout: 30000, + env: hermeticEnv(), }); // Check that settings.json was fixed @@ -83,6 +100,7 @@ describe('fix-cli --apply', () => { const result = execFileSync('node', [FIX_CLI, tmpDir, '--apply', '--json'], { encoding: 'utf-8', timeout: 30000, + env: hermeticEnv(), }); const output = JSON.parse(result); assert.ok(Array.isArray(output.verified), 'Should have verified array');