config-audit/tests/scanners/hook-additional-context.test.mjs
Kjell Tore Guttormsen d2c45a3bb8 feat(hooks): additionalContext injection advisory + filter-before lever (v5.10 B5) [skip-docs]
HKV now flags hooks that build hookSpecificOutput.additionalContext from
un-grepped command output as an INFO advisory (weight 0, never severity-bearing
— excluded from the self-audit nonInfo set). That field enters Claude's context
every time the hook fires (plain stdout on exit 0 does not), so an unfiltered
payload is a recurring per-turn token cost.

- New lib scanners/lib/hook-additional-context.mjs: pure assessHookAdditionalContext
  (unit-tested, no IO) + IO wrapper assessHookContextForRepo (walk hooks->scripts).
  Heuristic: additionalContext + verbose-prone capture (cat/git log/execSync/…) &&
  no filter (grep/head/jq/.slice). Deliberately low precision -> advisory only.
- HKV: emits the advisory inline on scripts it already reads (after the M5 verbose
  check). Additive, info severity -> frozen v5.0.0 + SC-5 snapshots untouched.
- feature-gap: new filterHookLeverFinding companion, fires ONLY when >=1 chatty
  hook is detected — surfaces the filter-before-Claude-reads lever (CC
  filter-test-output.sh). Silent otherwise (opportunity, not noise).
- docs: README HKV + GAP scanner rows; scanner-internals.md HKV row + full B5
  implementation note. CLAUDE.md kept lean ([skip-docs]); B5 fully documented in
  README + scanner-internals.

Mechanism verified 2026-06-23 against code.claude.com/docs context-window.md
(additionalContext enters context; plain stdout does not). Suite 1239 -> 1254 green.
Version/badges/CHANGELOG wait for the v5.10 release cut.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:28:33 +02:00

90 lines
3.5 KiB
JavaScript

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { assessHookAdditionalContext } from '../../scanners/lib/hook-additional-context.mjs';
// B5 (v5.10) — pure heuristic: does a hook script build
// hookSpecificOutput.additionalContext from un-grepped / verbose command output?
// Low precision by design (ships as an info advisory, never severity-bearing).
describe('assessHookAdditionalContext — not applicable', () => {
it('empty / missing input → not flagged', () => {
assert.equal(assessHookAdditionalContext({}).flagged, false);
assert.equal(assessHookAdditionalContext({ scriptContent: '' }).flagged, false);
assert.equal(assessHookAdditionalContext().flagged, false);
});
it('script with no additionalContext reference → not flagged even if it cats a file', () => {
const a = assessHookAdditionalContext({
scriptContent: 'CONTENT="$(cat /var/log/big.log)"\necho "$CONTENT"\n',
});
assert.equal(a.buildsAdditionalContext, false);
assert.equal(a.flagged, false);
});
});
describe('assessHookAdditionalContext — shell hooks', () => {
it('additionalContext built from an unfiltered cat → flagged', () => {
const a = assessHookAdditionalContext({
scriptContent:
'BODY="$(cat /tmp/notes.md)"\n' +
'echo "{\\"hookSpecificOutput\\":{\\"additionalContext\\":\\"$BODY\\"}}"\n',
});
assert.equal(a.buildsAdditionalContext, true);
assert.equal(a.hasVerboseCapture, true);
assert.equal(a.hasFilter, false);
assert.equal(a.capturesUnfiltered, true);
assert.equal(a.flagged, true);
});
it('additionalContext from a git log → flagged', () => {
const a = assessHookAdditionalContext({
scriptContent:
'LOG="$(git log --oneline)"\n' +
'printf \'{"hookSpecificOutput":{"additionalContext":"%s"}}\' "$LOG"\n',
});
assert.equal(a.flagged, true);
});
it('additionalContext from cat piped through grep → NOT flagged (filtered)', () => {
const a = assessHookAdditionalContext({
scriptContent:
'BODY="$(cat /tmp/test.log | grep ERROR)"\n' +
'echo "{\\"hookSpecificOutput\\":{\\"additionalContext\\":\\"$BODY\\"}}"\n',
});
assert.equal(a.hasFilter, true);
assert.equal(a.capturesUnfiltered, false);
assert.equal(a.flagged, false);
});
it('additionalContext from a cheap command only ($(date)) → NOT flagged', () => {
const a = assessHookAdditionalContext({
scriptContent:
'NOW="$(date)"\n' +
'echo "{\\"hookSpecificOutput\\":{\\"additionalContext\\":\\"$NOW\\"}}"\n',
});
assert.equal(a.hasVerboseCapture, false);
assert.equal(a.flagged, false);
});
});
describe('assessHookAdditionalContext — node hooks', () => {
it('execSync(git log) into additionalContext with no slice → flagged', () => {
const a = assessHookAdditionalContext({
scriptContent:
"const out = execSync('git log --oneline').toString();\n" +
"process.stdout.write(JSON.stringify({ hookSpecificOutput: { additionalContext: out } }));\n",
});
assert.equal(a.flagged, true);
});
it('readFileSync sliced before additionalContext → NOT flagged (bounded)', () => {
const a = assessHookAdditionalContext({
scriptContent:
"const raw = readFileSync('big.log', 'utf8');\n" +
"const out = raw.slice(0, 400);\n" +
"process.stdout.write(JSON.stringify({ hookSpecificOutput: { additionalContext: out } }));\n",
});
assert.equal(a.hasFilter, true);
assert.equal(a.flagged, false);
});
});