Follow-up, deliberately scoped out of the helper PR so its blast radius is chosen rather than inherited.
State on main today
hook::jq_fields strips NUL bytes from every value (landed with #2120). Twelve hooks call it. Zero consult any NUL signal, and eight own a block verdict:
HOOK jq_fields NUL-check exit 2
block-convention-violation 2 0 3
block-dangerous-git 2 0 7
block-hook-bypass 2 0 2
block-noncanonical-commit 2 0 5
block-no-verify 2 0 3
cli-flag-verify 2 0 1
flag-commit-pr-skill-bypass 2 0 0
hardcoded-path-check 2 0 2
secret-pattern-detection 2 0 2
skill-reference-verify 3 0 0
stale-path-verify 3 0 0
workflow-resilience-check 2 0 0
Ten of the twelve were newly converted by #2120; six of those own exit 2.
Why a signal is needed at all
Whatever the helper does with a NUL — strip it, truncate at it, or refuse — the caller is deciding a verdict on a value that is not the bytes the payload carried. Stripping joins text across the byte; truncating discards everything after it. Either way a hook that reports "scanned, clean" has scanned something other than what it was given, and today it cannot tell.
Two hooks read content fields with no upstream protection of any kind: secret-pattern-detection.sh and hardcoded-path-check.sh both take .tool_input.content, .new_string, .new_source, and both own exit 2. The control-character validation that exists in the harness covers command, script, and url only — and its own message names its purpose as hiding text from the human approval dialog, so it is an anti-spoofing measure rather than an input sanitizer.
What this issue asks for
Once the helper exposes a NUL signal, add the check to the callers — roughly three lines each — so a hook refuses rather than reporting a clean scan of a modified value.
It is not in the helper PR on purpose. Six verdict-owning hooks going fail-closed simultaneously is a real change in behaviour on main, and it deserves its own verification pass rather than riding along with a library change. Doing it in one sweep also means a single mistake affects every guard at once.
Suggested sequencing
- The helper exposes the signal (in progress).
- This issue: add checks to the two content-scanning verdict-owners first —
secret-pattern-detection.sh and hardcoded-path-check.sh — since those are the ones where a modified value can hide a credential or a machine path.
- Then the remaining verdict-owners.
- The four non-verdict hooks last, or never, if refusing is the wrong response for them — that is a per-hook judgement, not a sweep.
Measured, not assumed
The census above was run three times independently and agreed on twelve, zero, and eight. An earlier count of "four and two" was published and was wrong — it enumerated the hooks named in a review thread rather than the conversion set. Re-run it before acting rather than trusting this table; the file list moves.
exit 2 reachability through the content fields specifically was confirmed on both scanning hooks — a credential in .tool_input.content and in .new_string each produce exit 2, with clean controls at 0. Note when reproducing that hardcoded-path-check.sh returns early when CLAUDE_PROJECT_DIR is unset, so a probe without it exits 0 on every payload and reads exactly like "not reachable".
Follow-up, deliberately scoped out of the helper PR so its blast radius is chosen rather than inherited.
State on
maintodayhook::jq_fieldsstrips NUL bytes from every value (landed with #2120). Twelve hooks call it. Zero consult any NUL signal, and eight own a block verdict:Ten of the twelve were newly converted by #2120; six of those own
exit 2.Why a signal is needed at all
Whatever the helper does with a NUL — strip it, truncate at it, or refuse — the caller is deciding a verdict on a value that is not the bytes the payload carried. Stripping joins text across the byte; truncating discards everything after it. Either way a hook that reports "scanned, clean" has scanned something other than what it was given, and today it cannot tell.
Two hooks read content fields with no upstream protection of any kind:
secret-pattern-detection.shandhardcoded-path-check.shboth take.tool_input.content,.new_string,.new_source, and both ownexit 2. The control-character validation that exists in the harness coverscommand,script, andurlonly — and its own message names its purpose as hiding text from the human approval dialog, so it is an anti-spoofing measure rather than an input sanitizer.What this issue asks for
Once the helper exposes a NUL signal, add the check to the callers — roughly three lines each — so a hook refuses rather than reporting a clean scan of a modified value.
It is not in the helper PR on purpose. Six verdict-owning hooks going fail-closed simultaneously is a real change in behaviour on
main, and it deserves its own verification pass rather than riding along with a library change. Doing it in one sweep also means a single mistake affects every guard at once.Suggested sequencing
secret-pattern-detection.shandhardcoded-path-check.sh— since those are the ones where a modified value can hide a credential or a machine path.Measured, not assumed
The census above was run three times independently and agreed on twelve, zero, and eight. An earlier count of "four and two" was published and was wrong — it enumerated the hooks named in a review thread rather than the conversion set. Re-run it before acting rather than trusting this table; the file list moves.
exit 2reachability through the content fields specifically was confirmed on both scanning hooks — a credential in.tool_input.contentand in.new_stringeach produceexit 2, with clean controls at 0. Note when reproducing thathardcoded-path-check.shreturns early whenCLAUDE_PROJECT_DIRis unset, so a probe without it exits 0 on every payload and reads exactly like "not reachable".