Conversation
…ll, and said nothing (#186) pre-tool-hook.sh builds `full_command` from the tool_input keys, then cuts it at the first `;` `&` `|` or `"` to get `cmd` -- the command WORDS. A command that BEGINS with one of those bytes has no command words at all, and the hook answered `{}` and exited before the layer loop that consults any rule. That exit was a short-circuit on the wrong variable. `cmd` is the subject of exactly one consumer: the substring arm of the tool matcher. Three others read something else and were skipped by a test about a variable that is none of their business -- the regex arm, which matches the WHOLE command deliberately so that `cd x && git push` is reachable; the vocabulary pass, which lifts path tokens out of `command` and never touched `cmd`; and the per-row refusal notices, which need no subject. So a `mode: block` regex rule failed OPEN on a command beginning with one of the cut bytes: indexed, counted by every report, reading as enforced, never run. This is the only dimension that can refuse a call. The line is gone. The substring arm needs no exit of its own -- index("", term) is 0, and rebuild-tsv.sh refuses a row with an empty match column. THE CUT IS UNCHANGED. A substring rule still does not see past it: a rule about git push does not fire on a leading-semicolon command, exactly as it has never fired on `true; git push`. That cut is what keeps a substring rule off an echo of the same words (#7), and narrowing it to keep the first command word would fix this case by reopening that one. Section C pins it as kept rather than quietly narrowed. NO NEW NOTICE, and that is a decision. After this change no row is unreached: every row is read, the regex ones are evaluated and can fire, and a substring row that does not match is an ordinary non-match -- the same one `true; git push` has always produced silently. A notice here would fire on that whole class and would call rules unreachable that are working as #7 intends. The #182 census is untouched and still gated on the whole subject; section E drives both states apart. tests/test-cut-to-nothing.sh, 35 assertions across 6 sections, every must-not-fire assertion beside a must-fire control in the same tree, same hook. Red before the fix: PASS 26 FAIL 9, every failure a bare {} where a rule body was expected, every positive control green. Green after on one-true-awk 20200816, GNU Awk 5.4.1 and mawk. Full suite green, 2813 assertions, 0 skipped, exit 0. README.md gains the paragraph the table at "What a tool rule is tested against" was already promising, and the stale comment at the vocabulary gate -- which said this shape never reaches it -- is corrected. Closes #186 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fdaviddpt
pushed a commit
that referenced
this pull request
Aug 18, 2026
…ching (#191) A `<file>.sh:NNN` pointer is exact the day it is written and wrong on the next PR that inserts a line above it. It rots silently: a rotted citation reads exactly like a live one, so the reader follows it and lands on a plausible comment rather than on an error. Counted on main at 98386f1, outside the assembled changelog: nine citations of that shape. Six pointed at the wrong thing, one had drifted off the block it named, two were still right. #191 names three of the six; the sweep added here found the other three. #185 added one of them three hours before #190 moved it. Every citation in an enforced file now points at something greppable -- a function name, a distinctive literal, or the issue number, which additionally says why rather than where. tests/test-line-citations.sh has three outcomes rather than two. It FAILS on a citation in a tracked scripts or tests shell file; it REPORTS WITHOUT FAILING over every other tracked file; and it names the assembled changelog as deliberately not swept, because that file is generated and a finding in it is unactionable. The advisory half is advisory rather than enforced for one stated reason: its only remaining finding lives in an entry held by PR #192, and reddening a file this change may not edit is how a check gets disabled in its first week. Widening it is one awk pattern. The false-positive surface was costed before the check was written: over all 96 tracked basenames and the whole tree, 10 hits and 0 false. A bash diagnostic, a shellcheck line, an awk error and a longer basename are driven as controls that must not match, alongside a planted citation that must. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fdaviddpt
pushed a commit
that referenced
this pull request
Aug 18, 2026
- CLAUDE.md read as attributing the move to #191 itself, because the issue citation sat immediately after "the PR that moved it". #185 added it and #190 moved it three hours later; the sentence now says so. - scripts/jit-dry-run.sh: "which #7 is the reason for" -> "which exists because of #7". Accurate either way, clunky one way. A third finding was argued down rather than applied. The reviewer recounted the audit as 3 right / 1 drifted / 5 wrong and read the tooling.md citation as correct, on the strength of that entry own sentence "The truncation is pre-tool-hook.sh:127-144 and it is correct" -- where "it" is the truncation BEHAVIOUR being deliberate, not the line range being accurate. The same range in scripts/jit-dry-run.sh was judged wrong in the same review, which is the contradiction: at 98386f1 those lines are the #182 subagent_type comment, and the truncation is at 162-185. The count stands at 6 wrong / 1 drifted / 2 right. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fdaviddpt
added a commit
that referenced
this pull request
Aug 18, 2026
…ses new ones (#191) (#197) * Cross-file line-number citations rotted silently, and nothing was watching (#191) A `<file>.sh:NNN` pointer is exact the day it is written and wrong on the next PR that inserts a line above it. It rots silently: a rotted citation reads exactly like a live one, so the reader follows it and lands on a plausible comment rather than on an error. Counted on main at 98386f1, outside the assembled changelog: nine citations of that shape. Six pointed at the wrong thing, one had drifted off the block it named, two were still right. #191 names three of the six; the sweep added here found the other three. #185 added one of them three hours before #190 moved it. Every citation in an enforced file now points at something greppable -- a function name, a distinctive literal, or the issue number, which additionally says why rather than where. tests/test-line-citations.sh has three outcomes rather than two. It FAILS on a citation in a tracked scripts or tests shell file; it REPORTS WITHOUT FAILING over every other tracked file; and it names the assembled changelog as deliberately not swept, because that file is generated and a finding in it is unactionable. The advisory half is advisory rather than enforced for one stated reason: its only remaining finding lives in an entry held by PR #192, and reddening a file this change may not edit is how a check gets disabled in its first week. Widening it is one awk pattern. The false-positive surface was costed before the check was written: over all 96 tracked basenames and the whole tree, 10 hits and 0 false. A bash diagnostic, a shellcheck line, an awk error and a longer basename are driven as controls that must not match, alongside a planted citation that must. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Address the audit: an advisory sweep that could report coverage it never had (#191) Four findings from the self-review, all in tests/test-line-citations.sh unless noted: - The advisory sweep had no floor guard on its file count, so a broken selector would print `Clean across 0 file(s)` and exit 0 -- byte-identical, to run-all.sh which reads only the exit code, to a genuinely clean tree. It now takes the same `< 10 -> exit 2` floor the enforced sweep already had. - The enforced selector was `[^/]*`, so a shell file landing in a subdirectory one day would fall silently into the report-only bucket and stay there. Now `.*`, which closes the shape rather than half of it. - A tracked path that is not a readable file was skipped in silence in both loops, while the header above claimed a count the sweep never reached. Both are counted now; an unreadable path in the enforced set fails the suite. - The needle accepts an optional directory prefix, and no enforced file exercises that branch -- every real citation was bare-basename -- so a dead prefix branch would have read as clean. It has its own positive control now. scripts/jit-dry-run.sh named `jit_log_name()` and `jit_row_id()` as being "in pre-tool-hook.sh". They are defined in common.sh and only called there, which is an anchor wrong in the same way the line numbers were. Reworded to what is true: pre-tool-hook.sh routes every refused row through them. The 96-basename and 10-hit figures are now pinned to 98386f1 in all three places that carry them, rather than stated in the present tense. They were already stale by two the moment this branch added a file, which is the exact failure mode the change is about. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Two prose findings from the re-review (#191) - CLAUDE.md read as attributing the move to #191 itself, because the issue citation sat immediately after "the PR that moved it". #185 added it and #190 moved it three hours later; the sentence now says so. - scripts/jit-dry-run.sh: "which #7 is the reason for" -> "which exists because of #7". Accurate either way, clunky one way. A third finding was argued down rather than applied. The reviewer recounted the audit as 3 right / 1 drifted / 5 wrong and read the tooling.md citation as correct, on the strength of that entry own sentence "The truncation is pre-tool-hook.sh:127-144 and it is correct" -- where "it" is the truncation BEHAVIOUR being deliberate, not the line range being accurate. The same range in scripts/jit-dry-run.sh was judged wrong in the same review, which is the contradiction: at 98386f1 those lines are the #182 subagent_type comment, and the truncation is at 162-185. The count stands at 6 wrong / 1 drifted / 2 right. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * README said "Two suites are the exception" and this change made it three (#191) The test section enumerates the suites that are about this repository rather than the hooks, and the enumeration was a count. Adding tests/test-line-citations.sh made the sentence wrong in the quietest possible way -- a reader counting two and finding three has no reason to think anything is missing. Named rather than counted where it can be: the third one is called out by name and by what it refuses, and the "assembler they are about" clause now says which two it means. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Rebase onto 5b46095: two citations #194 wrote while this branch was open (#191) CI caught these, and the check that caught them is the one this branch adds. Neither is a defect in this diff -- they are in tests/test-session-markers.sh, section J, which #194 (issue #177) added to main after this branch was cut. - `session-start-hook.sh:32 parses the payload with jit_json_fields + jit_session_key` -- the line number was ALREADY WRONG on the day it was written: line 32 is mid-sentence in the LC_ALL paragraph, and the parse claim is three lines earlier. The comment names its own functions, so the number was carrying nothing the prose did not already carry. Dropped. - `the comment at session-start-hook.sh:25-29` -- accurate today, and a pointer to a comment rather than to code, which is the shape most likely to move. This one wanted more than a rename: it is a claim about TWO hooks agreeing, so it now names the pair of functions the claim is about and says which half of the claim this leg proves. The counts pinned to 98386f1 are re-pinned to 5b46095 in all three places that carry them, and the second measurement is kept beside the first rather than replacing it. Nine citations at 98386f1, eleven at 5b46095; 96 basenames / 10 hits / 0 false, then 98 / 12 / 0. Pinning was already the right call and the rebase is what proved it: the pin went stale in under a day and SAID SO, where a present-tense number would have read true and been wrong. That interval is now the argument the suite header leads with. The citations this check first went red on in CI were not the ones it was written for, they were written by somebody else while it was in review, and one of the two was born broken. A cleanup would not have caught either. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * The advisory half said it was blocked on #192, which has landed (#191) A reason that has been spent reads exactly like a reason that still holds -- which is the defect class this whole branch is about, in the file that exists to refuse it. The suite printed "advisory only while #192 holds one of the files it would flag" on every run, and #192 merged at eb9cf71. Replaced with what is true: widening is a scope decision nobody has taken, not a blocked one. Deliberately NOT widened here. It is unrequested scope on a pull request already in review, and the question it settles -- whether every future doc, template, example and jit-context entry is bound by this rule, in every installed project's contributor path -- is a design decision this branch was not briefed to make. Filed for the maintainer with the exact cost instead: two awk alternations in the selectors, one line in the entry currently flagged, and zero measured false positives on that surface across two commits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Florian DAVID <fdavid@digital-village.fr> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
scripts/pre-tool-hook.shbuilds a subjectfull_commandfrom the tool_input keys, then cuts it at the first;&|"or--to getcmd— the command words. A command that begins with one of those bytes has no command words at all:{"command":"; git push"}leavescmdempty whilefull_commandwas non-empty the whole time. The hook printed{}and exited before the layer loop that consults any rule.It was a short-circuit on the wrong variable, not a policy
cmdis the subject of exactly one consumer: the substring arm of the tool matcher. Three others read something else and were skipped by a test about a variable that is none of their business.full_command, deliberately — that is what makescd x && git pushreachable at all, and it is whatREADME.md's "What a tool rule is tested against" table has always promised. So a~matchrule would have matched and never ran, and because this is the only dimension that can refuse a call, amode: blockregex rule failed open on any command starting with a cut byte. Indexed, validated, counted by every diagnostic, reading as enforced, never run.command(the whole command) and never touchedcmd, so; cat src/Billing/x.phpbound nothing whiletrue; cat src/Billing/x.phpbound Billing the whole time. This one is not in the issue and is what settled the design: the question stops being "should the two matching arms agree" and becomes "why is a test about the substring arm's subject gating three consumers that do not read it".The line is deleted. The substring arm needs no exit of its own:
index("", term)is 0, andrebuild-tsv.sh:280refuses a row with an emptymatch:column, so no row's term can be empty.The three judgment calls the issue named
1. Should the regex arm run when the substring arm has nothing? Yes, and it is narrower than it sounds. A regex rule already matches the whole command on every other call —
~git pushalready fires onecho "git push"today, which is the price the README already documents for being able to anchor past a chain operator. The only calls whose verdict changes are those whose command words are empty, and on those the rule now does exactly what it does everywhere else. This is not new policy; it is the removal of an inconsistency.2. Does this state get its own report? No, and that is a decision rather than an omission. After the fix nothing is unreached: every row is read, the regex ones are evaluated and can fire, and a substring row that does not match is an ordinary non-match — the same one
true; git pushhas always produced silently. A notice here would fire on that entire class and would call rules unreachable that are working exactly as #7 intends. The #182 census is untouched and still gated on the whole subject; section E of the new suite drives the two states apart in one tree.3. Is the cut itself the bug? No. Cutting at the first
;&|"is what stops a substring rule aboutgit pushfiring onecho "git push"(#7). Narrowing it to keep the first command word would fix this case by reopening that one. Section C pins the cut as kept rather than quietly narrowed:git pushstill does not fire on; git push, exactly as it has never fired ontrue; git push.Tests
tests/test-cut-to-nothing.sh, 35 assertions across 6 sections. Every must-not-fire assertion sits beside a must-fire control in the same fixture, same tree, same hook —{}is what a dead tree, an unread layer and a hook that died all produce, and this issue is about not being able to tell those apart. Section B carries a negative control (; ls -la, a cut-to-nothing command the block rule does not describe) so it cannot pass by blocking everything. Every index is written byrebuild-tsv.shfrom real frontmatter.Red before the fix:
PASS: 26 FAIL: 9, every failure a{}where a rule body was expected, every positive control green. The reviewer reproduced that count independently against19f6c46. Green after:PASS: 35 FAIL: 0on one-true-awk 20200816, GNU Awk 5.4.1 and mawk (PATH-shimmed, darwin 24.3.0). Full suite green, 2813 assertions, 0 skipped, exit 0.Review
Two spawns against the committed diff. The auditor returned five classes, 0 findings, 0 could-not-check, and confirmed by reading
.github/workflows/tests.ymlthatrun-all.shruns onubuntu-latest,macos-latestandwindows-latest— so the new suite is covered on all three legs by CI, which no local run can establish.The reviewer found one real defect: the changelog fragment claimed 2817 full-suite assertions where the measured figure is 2813 (the first count used
grep -c 'PASS:', which also counts each suite's own summary line). Corrected and amended.Recorded rather than smoothed over: the reviewer's final message ended "beyond the one changelog number mismatch reported above" and that message contained no such statement — the item survived only because it was re-derivable. That is the partial form of the empty-return failure and it is in the report.
Not in this PR
scripts/jit-dry-run.sh:629,652andscripts/jit-doctor.sh:553cite line numbers inpre-tool-hook.shthat were already wrong on mergedmain(verified by reading them out ofgit show origin/main:), and this diff shifts them ~35 lines further. That is the class rather than the instance — a cross-file line-number citation convention in a repository whose files move every PR — and it wants its own issue and its own sweep.Closes #186
Verified by the maintainer
The premise I briefed was wrong, and the author is right. I framed this as "the two matching arms disagree about a cut-to-nothing subject" and handed over the evidence for it. The author found a third consumer that exit was skipping — the vocabulary pass, which lifts path tokens out of the whole
commandand never readcmdat all, so; cat src/Billing/x.phpbound nothing whiletrue; cat src/Billing/x.phpbound the whole time. That reframes the defect from a disagreement between two arms into one short-circuit on a variable that is only one consumer's business, which is also why the fix is a deletion rather than a reconciliation. Nothing in my brief pointed there.Red re-run, mine, against merged
mainat 19f6c46 with the fix absent:PASS: 26 FAIL: 9, every failure a{}where a rule body was expected. That is the same count the author measured and the same count their reviewer reproduced independently — three derivations, one number.I accept the two refusals in this diff, and they are the harder half of it. Not adding a notice is right: after the deletion nothing is unreached, and a notice on this class would call working rules unreachable — it would fire on the ordinary non-match that
true; git pushhas always produced. Keeping the cut is right for the same reason it was introduced in #7, and section C pins it as kept rather than quietly narrowed, which is the assertion I would have asked for if it were not already there.The reviewer went partially dark again — its final message referred to "the one changelog number mismatch reported above" and contained no such statement. The item survived only because it was re-derivable by re-counting the suite log. That is the third reviewer in this session to name findings it did not state, and the author recorded it as
partialrather than rounding it to clean, which is the correct call.Filed rather than carried: #191 — the stale cross-file line-number citations in
jit-dry-run.shandjit-doctor.sh. All three were already wrong on mergedmainbefore this diff, verified by reading them out ofgit show origin/main:; this diff shifts them further. It is the convention that needs deciding, not the four numbers.