feat(config-audit): HKV flags verbose hook output (v5 M5) [skip-docs]
Static heuristic — counts console.log / process.stdout.write lines per referenced hook script. > 50 → low CA-HKV-NNN finding. New fixtures: - hooks-verbose/ (61 verbose lines → triggers) - hooks-quiet/ (5 lines → no finding) 580 → 582 tests, all green.
This commit is contained in:
parent
7181862644
commit
910567d661
6 changed files with 153 additions and 0 deletions
|
|
@ -36,6 +36,11 @@ const VALID_TYPES = new Set(['command', 'http', 'prompt', 'agent']);
|
|||
const MIN_TIMEOUT = 1000;
|
||||
const MAX_TIMEOUT = 300000; // 5 minutes
|
||||
|
||||
/** v5 M5: hook scripts that flood stdout fragment the cache prefix on every
|
||||
* fire and slow Claude Code's UI. Static heuristic — count log lines. */
|
||||
const VERBOSE_HOOK_LINE_THRESHOLD = 50;
|
||||
const VERBOSE_HOOK_LINE_RX = /\b(?:console\.log|process\.stdout\.write)\s*\(/;
|
||||
|
||||
/**
|
||||
* Scan all hooks.json files and hook configs in settings.json.
|
||||
* @param {string} targetPath
|
||||
|
|
@ -198,8 +203,10 @@ async function validateHooksObject(hooks, file, findings, baseDir) {
|
|||
if (hook.type === 'command' && hook.command) {
|
||||
const scriptPath = extractScriptPath(hook.command, baseDir);
|
||||
if (scriptPath) {
|
||||
let scriptExists = false;
|
||||
try {
|
||||
await stat(scriptPath);
|
||||
scriptExists = true;
|
||||
} catch {
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
|
|
@ -212,6 +219,31 @@ async function validateHooksObject(hooks, file, findings, baseDir) {
|
|||
autoFixable: false,
|
||||
}));
|
||||
}
|
||||
|
||||
// v5 M5: count verbose stdout writes when the script exists.
|
||||
if (scriptExists) {
|
||||
const verboseCount = await countVerboseLines(scriptPath);
|
||||
if (verboseCount > VERBOSE_HOOK_LINE_THRESHOLD) {
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity: SEVERITY.low,
|
||||
title: 'Verbose hook output (loud script)',
|
||||
description:
|
||||
`${file.relPath}: "${event}" runs ${scriptPath.split('/').slice(-2).join('/')} ` +
|
||||
`which has ${verboseCount} console.log / process.stdout.write lines ` +
|
||||
`(>${VERBOSE_HOOK_LINE_THRESHOLD}). Loud hooks slow the UI and bloat ` +
|
||||
'session transcripts on every fire.',
|
||||
file: scriptPath,
|
||||
evidence:
|
||||
`console_log_or_stdout_lines=${verboseCount}; ` +
|
||||
`threshold=${VERBOSE_HOOK_LINE_THRESHOLD}`,
|
||||
recommendation:
|
||||
'Trim debug logging from hooks. Keep hook output to actionable signals; ' +
|
||||
'route verbose diagnostics to a log file instead of stdout.',
|
||||
autoFixable: false,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -246,6 +278,20 @@ async function validateHooksObject(hooks, file, findings, baseDir) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count lines containing console.log( or process.stdout.write( in a hook script.
|
||||
* Static heuristic — does not execute the script.
|
||||
*/
|
||||
async function countVerboseLines(scriptPath) {
|
||||
const content = await readTextFile(scriptPath);
|
||||
if (!content) return 0;
|
||||
let count = 0;
|
||||
for (const line of content.split('\n')) {
|
||||
if (VERBOSE_HOOK_LINE_RX.test(line)) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a filesystem path from a hook command string.
|
||||
* Handles ${CLAUDE_PLUGIN_ROOT} variable substitution.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue