Preserve the original contributor histories and apply the exact independently reviewed repair.
Source-Parent: dee884656e
Review-Manifest-SHA256: 0c7eeef7c2722a605d049daf2dc3c2473c10dd616005afcba21049fc036d8ce9
Preserve original contributor history and all current-main assertions while bounding quote, heredoc, shell-option and deferred-scan handling.
Source-PR: https://github.com/affaan-m/ECC/pull/2965
Source-Parent: 5a878131de
Main-Parent: d3b8a3e908
Review-Manifest-SHA256: 5d23f47e60516b66733805aec0ebcd47f19265c964529fa0b1220c8aa6903add
Two cases the word-level rewrite still got wrong. Short options that take an optional stuck value (-u[mode], -S[keyid]) end the cluster scan, so git commit -uno and -Sn are allowed while -nu stays blocked. Git accepts any unambiguous long-option prefix, so --no-veri and --no-verif on commit, push, merge and rebase are now blocked; --no-verbose stays allowed. Quoted data such as -m "--no-verify" is still treated as data. Independent exact-head review probed 34 commands in-process and against real git with no bypass and no false positive; hook test 35/35, eslint clean, CI 44/44 at the head.
Merge buildCommentMask into buildScanBoundaries so one pass owns comment mask and scan bounds.
Extract shell-scan primitives to scripts/hooks/lib/shell-scan.js; keep policy in the hook.
Use a sticky heredoc regex instead of copying the remaining input on every <<.
Replace the short-circuiting chain test with clean-leading sequences that exercise checkCommand advance.
Add quoted-executable non-leakage coverage for the scanEnd=length path.
Treat $'…' ANSI-C quoting as a quoted executable word.
Join unquoted backslash-newline for the flag scan and heredoc-line bound.
Co-Authored-By: grok (fleet lane) <noreply@fleet.local>
Co-authored-by: groy75 <groy75@users.noreply.github.com>
The scanner locked onto a `git commit` literal that sat inside a quoted
string of an OUTER command (a python heredoc's string, a VAR="..."
assignment, a printf JSON payload), then hasNoVerifyFlag() re-tokenized from
that inner offset with a fresh quote state. When quote parity flipped, the
"segment" ran past the enclosing string and picked up an unrelated later
`bash -n` / `sed -n` / `grep -n` as `commit -n`. Two legitimate commands were
blocked this way in one working session.
The first attempt at this PR (d5c428c) fixed the false positive by SKIPPING
`git` tokens judged to be in "data" context. That was the wrong lever: data
becomes executable the moment it is piped to a shell (`echo '...' | bash`,
`bash <<< '...'`, `cat <<EOF | bash`), and no regex list of shell invocations
is complete (`bash --noprofile -c` slipped). Three independent reviewers
(codex, Greptile, CodeRabbit) demonstrated executing bypasses; that commit is
squashed away here and none of its behaviour survives.
This version keeps every `git` candidate visible and keeps main's blocking
surface intact. A single O(n) preprocessing pass (buildScanBoundaries)
records, for each enclosed character, the closing quote or the current
heredoc-body line end. Subcommand discovery and flag tokenization are capped
at that boundary, so tokens after the enclosing data can never leak into a
candidate's argument list. Also: comment detection is a single mask pass
instead of a repeated prefix scan; heredoc parsing accepts quoted hyphenated
delimiters and CRLF terminators and preserves the `<<` vs `<<-` tab
distinction; quote-assembled words (`g''it`) are recognized.
Verified (all payloads run through the hook as JSON on stdin):
- red-list, 24 executing forms incl. every reviewer-reported bypass -> exit 2
on main AND here (CRLF heredoc trailer: 0 on main, 2 here);
- green-list, 16 leak-class false positives -> exit 2 on main, 0 here;
- tests/hooks/block-no-verify.test.js 84/84 (main's 25 retained verbatim,
d5c428c's 8 removed, 59 table-driven red/green cases added);
tests/hooks/cursor-block-no-verify.test.js 14/14;
- 204,807-byte command with 8,905 non-command `git` literals: 8.5 ms.
Co-Authored-By: Codex (GPT) <noreply@openai.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EyuZY1LZNuShHiewExekLT
isCommitNoVerifyShortFlag anchored on the first character, so it only recognised the flag when it led the cluster. Git clusters short options, which means git commit -an is -a plus the bypass flag and skips the hooks. -sn and -vn slip through the same way, while -na and -nm are caught — the difference is position, not intent.
Scanning now walks the cluster and stops at a value-taking option, since that option swallows the rest as its inline value. The n in -mn stays message text, and the existing -tn case keeps working.
Adds 4 tests: the three clustered forms that were escaping, plus -mn to pin the inline-value boundary. Verified the three fail against current main. Suite 25 to 29.
Co-authored-by: haelyra <49814733+haelyra@users.noreply.github.com>
Backport Jamkris's fix for case-insensitive core.hooksPath overrides and the git commit -tn template-path false positive. Verified locally on current main with 25/25 block-no-verify tests and node tests/run-all.js passing 2369/2369.
- Add `minimal` profile so the security hook runs in all profiles
- Scope -n/--no-verify flag check to the detected subcommand region,
preventing false positives on chained commands (e.g. `git log -n 10`)
- Guard stdin listeners with `require.main === module` so require()
from run-with-flags.js does not register unnecessary listeners
- Verify subcommand token is preceded only by flags/flag-args after
"git", preventing misclassification of argument values as subcommands
- Add integration tests for block-no-verify hook
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace inline `npx block-no-verify@1.1.2` with a standalone Node.js
script routed through `run-with-flags.js`, matching every other hook.
Fixes two bugs:
1. npx inherits the project cwd and triggers EBADDEVENGINES in
pnpm-only projects that set devEngines.packageManager.onFail=error.
2. The hook bypassed run-with-flags.js so ECC_DISABLED_HOOKS had no
effect — the isHookEnabled() check never ran.
The new script replicates the full block-no-verify@1.1.2 detection
logic (--no-verify, -n shorthand for commit, core.hooksPath override)
with zero external dependencies.
Closes#1378