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>
This commit is contained in:
parent
8f7e196046
commit
d2c45a3bb8
11 changed files with 468 additions and 4 deletions
|
|
@ -12,7 +12,7 @@ Scanner CLI: `node scanners/scan-orchestrator.mjs <path> [--global] [--full-mach
|
|||
|---------|--------|---------|
|
||||
| `claude-md-linter.mjs` | CML | Structure, length, sections, @imports, duplicates, TODOs |
|
||||
| `settings-validator.mjs` | SET | Schema, unknown/deprecated keys, type mismatches, permissions |
|
||||
| `hook-validator.mjs` | HKV | Format, script existence, event validity, timeouts |
|
||||
| `hook-validator.mjs` | HKV | Format, script existence, event validity, timeouts, verbose-stdout (low), unfiltered `additionalContext` injection (info advisory, v5.10 B5) |
|
||||
| `rules-validator.mjs` | RUL | Glob matching, orphan rules, deprecated fields, unscoped rules |
|
||||
| `mcp-config-validator.mjs` | MCP | Server types, env vars, unknown fields |
|
||||
| `import-resolver.mjs` | IMP | Broken @imports, circular refs, deep chains, tilde paths |
|
||||
|
|
@ -185,6 +185,40 @@ operations (CLI adds zero context tokens until invoked). Deferred MCP is effecti
|
|||
lever stays silent in the default case (opportunity, not noise — mirrors the bundledSkills lever).
|
||||
`alwaysLoad` was added to CA-MCP's `VALID_SERVER_FIELDS` so it is never flagged as an unknown field.
|
||||
|
||||
### hook-validator — unfiltered additionalContext advisory (v5.10 B5)
|
||||
|
||||
A hook that emits `hookSpecificOutput.additionalContext` has that payload injected into Claude's
|
||||
context **every time it fires** — plain stdout on exit 0 does NOT (it goes to the debug log only). A
|
||||
hook that dumps large, un-grepped command output into `additionalContext` is therefore a recurring,
|
||||
compaction-sensitive per-turn token cost. HKV flags it as an **`info` advisory** (weight 0 — never
|
||||
severity-bearing, excluded from the self-audit `nonInfo` set), paired with a feature-gap lever.
|
||||
|
||||
The heuristic lives in `lib/hook-additional-context.mjs` as a **pure** `assessHookAdditionalContext({scriptContent})`
|
||||
(unit-tested, no IO) plus a thin IO wrapper `assessHookContextForRepo(discovery)` (walk hooks → scripts
|
||||
→ assess) used by GAP; HKV calls the pure function inline on scripts it already reads. The signal:
|
||||
|
||||
| Condition | Detected by | Effect |
|
||||
|-----------|-------------|--------|
|
||||
| references `additionalContext` | `/additionalContext/` | gate (else not applicable) |
|
||||
| captures verbose-prone output | `cat`/`find`/`ls`/`git log\|diff\|status\|show`/`npm`/`pytest`/`jest`/`curl`/`execSync`/`readFileSync`… | `hasVerboseCapture` |
|
||||
| applies any truncating filter | `grep`/`head`/`tail`/`sed`/`awk`/`jq`/`cut`/`wc`/`uniq`/`sort` or `.slice`/`.substring` | suppresses (assumed bounded) |
|
||||
|
||||
`flagged = buildsAdditionalContext && hasVerboseCapture && !hasFilter`. A filtered capture (e.g.
|
||||
`cat … | grep ERROR`) or a cheap-only capture (`$(date)`) is not flagged.
|
||||
|
||||
**Why `info`, not a hard finding (Verifiseringsplikt).** This is deliberately **low precision** — a
|
||||
static scan cannot run the hook or measure the real payload, and a filter we don't recognise would be
|
||||
a false positive. So it ships as an advisory with the precision caveat in its own description, never a
|
||||
graded/severity-bearing finding. Mechanism verified 2026-06-23 against `code.claude.com/docs`:
|
||||
`context-window.md` — *"A PostToolUse hook … reports back via `hookSpecificOutput.additionalContext`.
|
||||
That field enters Claude's context. Plain stdout on exit 0 does not."* + the tip to keep output concise
|
||||
(it enters context without truncation).
|
||||
|
||||
**feature-gap companion.** `filterHookLeverFinding` (GAP) fires **only** when `assessHookContextForRepo`
|
||||
returns ≥1 chatty hook — surfaces the documented **filter-before-Claude-reads** lever (`filter-test-output.sh`:
|
||||
grep ERROR and return only matches instead of a 10,000-line log). No chatty hook → silent (opportunity,
|
||||
not noise — same contract as the cliOverMcp / bundledSkills levers).
|
||||
|
||||
### CML scanner — context-window-scaled char budget
|
||||
|
||||
Beyond the line-count checks (200/500 lines, both MEDIUM), the CML scanner mirrors
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue