Skip to content

fix: close two block-no-verify bypass holes (core.hooksPath case, -tn) - #1843

Merged
affaan-m merged 1 commit into
affaan-m:mainfrom
Jamkris:fix/block-no-verify-bypass-holes
May 13, 2026
Merged

fix: close two block-no-verify bypass holes (core.hooksPath case, -tn)#1843
affaan-m merged 1 commit into
affaan-m:mainfrom
Jamkris:fix/block-no-verify-bypass-holes

Conversation

@Jamkris

@Jamkris Jamkris commented May 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Two real bypass holes in scripts/hooks/block-no-verify.js, surfaced during a CodeRabbit round-3 review on the downstream EGC port (Jamkris/everything-gemini-code#68). Both ship in current main.

Bug 1 — core.hooksPath case-sensitivity bypass (critical)

GIT_CONFIG_KEY_PREFIX was compared case-sensitively, but Git config section and variable names are case-insensitive (https://git-scm.com/docs/git-config"The variable names are case-insensitive"). The following both slipped past the guard:

git -c core.hookspath=/dev/null commit -m "msg"
git -c core.HOOKSPATH=/dev/null commit -m "msg"

Fix: lowercase the prefix constant and compare candidate tokens in lowercase form inside hasHooksPathOverride (both the -c <key> and -c<key> code paths).

Bug 2 — -tn false positive (major)

COMMIT_SHORT_OPTIONS_WITH_VALUE was missing 't'. Git parses git commit -tn as -t with template path 'n' (stuck-arg form per https://git-scm.com/docs/api-parse-options), not as -t -n. Without 't' in the set, isCommitNoVerifyShortFlag reached the n check and falsely flagged the command as a --no-verify bypass — breaking legitimate -t <template> usage when the template path starts with the letter n (or is just the letter n).

Fix: add 't' to COMMIT_SHORT_OPTIONS_WITH_VALUE so the scanner treats the rest of the combined-short token as the option's value and stops scanning before checking for n.

Tests (+3, 25 total in tests/hooks/block-no-verify.test.js)

  • blocks case-variant core.hooksPath (lowercase)git -c core.hookspath=/dev/null commit -m "msg" → exit 2
  • blocks case-variant core.hooksPath (uppercase)git -c core.HOOKSPATH=/dev/null commit -m "msg" → exit 2
  • still allows -tn (n is the -t template path, not a flag)git commit -tn -m "msg" → exit 0

Local verification

  • yarn lint clean
  • All CI validators pass: validate-agents, validate-commands, validate-rules, validate-skills, validate-hooks, validate-install-manifests, validate-no-personal-paths
  • node tests/run-all.js: 2369/2369 pass including the 3 new block-no-verify cases
  • Caveat (unrelated): yarn test currently fails at check-unicode-safety on skills/windows-desktop-e2e/SKILL.md (U+2605 stars) which landed in commit 1481628. That failure exists on main independently of this PR and would also fail without these changes — flagging it here in case it's news.

Provenance

Originally discovered and fixed downstream in Jamkris/everything-gemini-code#68 (commit fbf7908). Backporting here since the same code path exists upstream. The downstream port has been running with these fixes against EGC's own pre-commit hooks for a day with no false-positive reports.

Happy to split into two PRs (one critical, one major) if that's easier to triage — the two changes are independent. Also happy to drop the new tests or rework them in any direction.


Summary by cubic

Fixes two bypass holes in scripts/hooks/block-no-verify.js to correctly block core.hooksPath overrides and stop false positives for git commit -tn. This strengthens hook enforcement without breaking valid commits.

  • Bug Fixes
    • Treat core.hooksPath as case-insensitive when parsing -c (supports both -c key=... and -ckey=... forms).
    • Add -t to short options that take a value so -tn is parsed as a template path, not -n.

Written for commit ff37e40. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Improved git hook bypass protection with enhanced detection for configuration overrides and commit options.
  • Tests

    • Added test coverage for edge cases in git hook bypass detection scenarios.

Review Change Stack

Two real bypass holes in scripts/hooks/block-no-verify.js, surfaced
during a CodeRabbit round-3 review on the downstream EGC port
(Jamkris/everything-gemini-code#68). Both ship in current main.

Bug 1 — core.hooksPath case-sensitivity bypass (critical)

  GIT_CONFIG_KEY_PREFIX was compared case-sensitively, but Git
  config section and variable names are case-insensitive
  (https://git-scm.com/docs/git-config — "The variable names are
  case-insensitive"). The following both slipped past the guard:

    git -c core.hookspath=/dev/null commit -m "msg"
    git -c core.HOOKSPATH=/dev/null commit -m "msg"

  Fix: lowercase the prefix constant and compare candidate tokens
  in lowercase form inside hasHooksPathOverride (both the
  "-c <key>" and "-c<key>" code paths).

Bug 2 — -tn false positive (major)

  COMMIT_SHORT_OPTIONS_WITH_VALUE was missing 't'. Git parses
  `git commit -tn` as -t with template path 'n' (stuck-arg form
  per git-scm.com/docs/api-parse-options), not as `-t -n`. Without
  't' in the set, isCommitNoVerifyShortFlag reached the 'n' check
  and falsely flagged the command as a --no-verify bypass —
  breaking legitimate `-t <template>` usage when the template path
  starts with the letter n (or is just the letter n).

  Fix: add 't' to COMMIT_SHORT_OPTIONS_WITH_VALUE so the scanner
  treats the rest of the combined-short token as the option's
  value and stops scanning before checking for 'n'.

Tests (+3, 25 total in tests/hooks/block-no-verify.test.js):

  - blocks case-variant core.hooksPath (lowercase)
  - blocks case-variant core.hooksPath (uppercase)
  - still allows -tn (n is the -t template path, not a flag)

Local verification:
  - yarn lint clean
  - all CI validators pass (validate-agents, validate-commands,
    validate-rules, validate-skills, validate-hooks,
    validate-install-manifests, validate-no-personal-paths)
  - node tests/run-all.js: 2369/2369 pass including the 3 new
    block-no-verify cases

Caveat: yarn test currently fails at check-unicode-safety on
skills/windows-desktop-e2e/SKILL.md (U+2605 stars) which landed in
commit 1481628 — independent of this PR, but worth flagging.

Provenance: originally discovered and fixed downstream in
Jamkris/everything-gemini-code#68 (commit fbf7908). Backporting
here since the same code path exists upstream.
@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR hardens git hook bypass detection in block-no-verify.js by making config key matching case-insensitive and extending short-option parsing to handle the -t template option correctly, preventing bypass attempts using alternative casing and avoiding false positives on combined option tokens.

Changes

Git Hook Bypass Detection Hardening

Layer / File(s) Summary
Documentation and short-option parsing hardening
scripts/hooks/block-no-verify.js
Added clarifying comments documenting case-insensitive git config key normalization, and extended COMMIT_SHORT_OPTIONS_WITH_VALUE to include 't' to correctly parse combined short options like -tn as a template option consuming the following value.
Case-insensitive hooks-path override detection
scripts/hooks/block-no-verify.js
Reworked hasHooksPathOverride to lowercase candidate tokens and detect -c overrides of core.hookspath= in both two-token (-c core.hookspath=...) and inline (-ccore.hookspath=...) forms, preventing bypass attempts using different casing.
Test coverage for hardening measures
tests/hooks/block-no-verify.test.js
Added test cases validating case-insensitive handling of core.hooksPath in -c overrides (lowercase and uppercase variants) and a regression test ensuring -tn is correctly parsed as a template option and does not trigger bypass blocking.

Possibly Related PRs

  • affaan-m/everything-claude-code#1385: The main PR further hardens/extends the same scripts/hooks/block-no-verify.js git-override detection (case-insensitive core.hooksPath matching and updated short-option parsing), which builds directly on the detection logic introduced in the retrieved PR's new block-no-verify implementation.
  • affaan-m/everything-claude-code#1628: The main PR's updates to scripts/hooks/block-no-verify.js (case-insensitive core.hooksPath -c override matching and adjusting commit-option value consumption via COMMIT_SHORT_OPTIONS_WITH_VALUE) build directly on the retrieved PR's shell-word/token parsing of --no-verify/-n and hooks-path overrides.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

A rabbit hops through git commands with glee, 🐰
Now catching bypasses you can't plainly see,
With lowercase matching and options so tight,
Those -t and -c tricks won't slip left or right!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically summarizes the main changes: fixing two security bypass holes in the block-no-verify hook detection (core.hooksPath case-sensitivity and -tn parsing issue).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/hooks/block-no-verify.js`:
- Around line 75-80: The current token scanner only checks whole-token `-n` and
misses `-an`/`-anm` clusters; update the short-option cluster parsing to iterate
characters after the leading '-' in each token (use the existing
COMMIT_SHORT_OPTIONS_WITH_VALUE set), process options sequentially: if a
character is 'n' immediately mark/return as --no-verify, if a character is in
COMMIT_SHORT_OPTIONS_WITH_VALUE treat the remainder of the token as that
option's value and stop scanning that token, otherwise continue to the next
character; apply this logic where tokens are scanned for no-verify to ensure
clusters like '-anm' and '-an' are correctly blocked.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6aab3fd1-76d3-42a9-9795-087d36767641

📥 Commits

Reviewing files that changed from the base of the PR and between daf0355 and ff37e40.

📒 Files selected for processing (2)
  • scripts/hooks/block-no-verify.js
  • tests/hooks/block-no-verify.test.js

Comment on lines +75 to +80
// Short options that take a value. When seen as part of a combined
// short-option token (e.g. -tn), git's parser treats the rest of the
// token as the option's value (template path 'n' here), so the scanner
// must stop at this character — anything after it is the inline value,
// not another flag.
const COMMIT_SHORT_OPTIONS_WITH_VALUE = new Set(['m', 'F', 'C', 'c', 't']);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In Git, how are combined short options parsed for git commit`? Please verify from official Git documentation whether:

  1. git commit -an -m "msg" is equivalent to git commit -a -n -m "msg";
  2. git commit -anm "msg" is equivalent to git commit -a -n -m "msg";
  3. git commit -tn -m "msg" is parsed as -t n -m "msg" rather than -t -n -m "msg".`

💡 Result:

Yes, all three are true according to Git's official parse-options documentation and source code. 1. git commit -an -m "msg" is equivalent to git commit -a -n -m "msg": Yes. Short options can be bundled/clustered after a single dash, so -an is parsed as -a followed by -n [1][2][3]. 2. git commit -anm "msg" is equivalent to git commit -a -n -m "msg": Yes. Bundling continues across multiple option groups; -anm processes -a, then -n, then -m, with "msg" as the argument to -m since -m requires one [1][3][4]. 3. git commit -tn -m "msg" is parsed as -t n -m "msg" rather than -t -n -m "msg": Yes. The -t option (for --template) requires an argument. In -tn, after processing -t, the next character n (with no dash) is taken as its required argument from the same argv entry (stuck form). -n is not processed here [5][6][3]. Git's parser uses parse_short_opt() which matches each short option char sequentially, advancing the pointer after each. For options needing args, get_value() peeks at the next char in the bundle or next argv as the arg [3]. This matches standard Unix/POSIX getopt-style parsing for short options [7].

Citations:


Combined short tokens still allow a --no-verify bypass.

git commit -an -m "msg" and git commit -anm "msg" still slip through. The scanner only recognizes -n when the token starts with -n. According to Git's official parse-options parser, short options are processed sequentially within a cluster; when an option requires a value (like -m or -t), the remainder of the token becomes its argument. The check must iterate through the cluster and block n unless an earlier value-taking option has already consumed the remainder.

Suggested fix
 function isCommitNoVerifyShortFlag(value) {
-  return value === '-n' || /^-n[a-zA-Z]/.test(value);
+  if (!value.startsWith('-') || value.startsWith('--') || value === '-') {
+    return false;
+  }
+
+  for (const option of value.slice(1)) {
+    if (option === 'n') {
+      return true;
+    }
+    if (COMMIT_SHORT_OPTIONS_WITH_VALUE.has(option)) {
+      return false;
+    }
+  }
+
+  return false;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/hooks/block-no-verify.js` around lines 75 - 80, The current token
scanner only checks whole-token `-n` and misses `-an`/`-anm` clusters; update
the short-option cluster parsing to iterate characters after the leading '-' in
each token (use the existing COMMIT_SHORT_OPTIONS_WITH_VALUE set), process
options sequentially: if a character is 'n' immediately mark/return as
--no-verify, if a character is in COMMIT_SHORT_OPTIONS_WITH_VALUE treat the
remainder of the token as that option's value and stop scanning that token,
otherwise continue to the next character; apply this logic where tokens are
scanned for no-verify to ensure clusters like '-anm' and '-an' are correctly
blocked.

@greptile-apps

greptile-apps Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR closes two bypass holes in the block-no-verify.js PreToolUse hook that protects against AI agents skipping git hooks.

  • Case-insensitivity fix: GIT_CONFIG_KEY_PREFIX is now lowercased and all token comparisons in hasHooksPathOverride apply .toLowerCase() before matching, closing a real bypass via mixed-case config keys. Both new case-variant tests fail against the unfixed code, confirming the bug is real.
  • 't' added to COMMIT_SHORT_OPTIONS_WITH_VALUE: spec-correct change so combined tokens like -tn are treated as -t consuming n as its template value. The described false positive does not appear to trigger in this codebase because isCommitNoVerifyShortFlag only matches strings starting with -n. The inline sticky-form case-variant is now handled by the lowered branch but lacks a dedicated test.

Confidence Score: 4/5

Safe to merge — the case-sensitivity change is a genuine security improvement and all existing tests continue to pass.

The core.hooksPath case-insensitivity fix is correct and closes a real bypass hole, confirmed by the two new tests that fail on the unfixed code. The t-in-set change is semantically correct. Minor gaps remain: the -tn test does not demonstrate an observable regression in this codebase, and the inline sticky-form case-variant has no dedicated test.

The test file would benefit from one more test covering the inline mixed-case form git -ccore.HOOKSPATH= commit.

Important Files Changed

Filename Overview
scripts/hooks/block-no-verify.js Two targeted security fixes: GIT_CONFIG_KEY_PREFIX lowercased and comparisons in hasHooksPathOverride normalized via .toLowerCase() to close a real case-insensitive bypass hole; 't' added to COMMIT_SHORT_OPTIONS_WITH_VALUE so -tn-style tokens are correctly treated as -t with inline value.
tests/hooks/block-no-verify.test.js Three new tests added; the two case-sensitivity tests are genuine regression guards. The -tn test is correct behavior but would also pass against the unfixed code. The inline mixed-case -c form has no corresponding test.

Reviews (1): Last reviewed commit: "fix: close two block-no-verify bypass ho..." | Re-trigger Greptile

Comment on lines +184 to +198
if (test('blocks case-variant core.hooksPath (lowercase)', () => {
const r = runHook({ tool_input: { command: 'git -c core.hookspath=/dev/null commit -m "msg"' } });
assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`);
assert.ok(/core\.hookspath/i.test(r.stderr), `stderr should mention core.hooksPath: ${r.stderr}`);
})) passed++; else failed++;

if (test('blocks case-variant core.hooksPath (uppercase)', () => {
const r = runHook({ tool_input: { command: 'git -c core.HOOKSPATH=/dev/null commit -m "msg"' } });
assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`);
})) passed++; else failed++;

if (test('still allows -tn (n is the -t template path, not a flag)', () => {
const r = runHook({ tool_input: { command: 'git commit -tn -m "msg"' } });
assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`);
})) passed++; else failed++;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 -tn test was already passing before this fix

Tracing git commit -tn -m "msg" through the OLD code: isCommitNoVerifyShortFlag('-tn') checks value === '-n' (false) and /^-n[a-zA-Z]/.test('-tn') (false — the regex requires the second character to be n, but -tn has t there). Neither branch fires, so the token falls through without blocking. The test would pass before and after the patch. The 't'-in-set fix is still the right semantic change, but the stated false-positive scenario does not manifest in this codebase's current isCommitNoVerifyShortFlag — worth documenting so future reviewers don't assume the test is a regression guard for an observable failure.

Comment on lines +190 to +193
if (test('blocks case-variant core.hooksPath (uppercase)', () => {
const r = runHook({ tool_input: { command: 'git -c core.HOOKSPATH=/dev/null commit -m "msg"' } });
assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`);
})) passed++; else failed++;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No test for inline -c combined form with mixed-case key

The lowered.startsWith('-ccore.hookspath=') branch in hasHooksPathOverride now correctly handles git -ccore.HOOKSPATH=/dev/null commit, but there is no test covering this inline (no-space) form with mixed case. The existing suite only tests the space-separated form. A case-variant bypass via the sticky form would silently slip through if a future refactor broke this branch.

Suggested change
if (test('blocks case-variant core.hooksPath (uppercase)', () => {
const r = runHook({ tool_input: { command: 'git -c core.HOOKSPATH=/dev/null commit -m "msg"' } });
assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`);
})) passed++; else failed++;
if (test('blocks case-variant core.hooksPath (uppercase)', () => {
const r = runHook({ tool_input: { command: 'git -c core.HOOKSPATH=/dev/null commit -m "msg"' } });
assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`);
})) passed++; else failed++;
if (test('blocks inline -c form with mixed-case key (e.g. -ccore.HOOKSPATH=)', () => {
const r = runHook({ tool_input: { command: 'git -ccore.HOOKSPATH=/dev/null commit -m "msg"' } });
assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`);
})) passed++; else failed++;

@affaan-m
affaan-m merged commit 6be241a into affaan-m:main May 13, 2026
4 checks passed
Jamkris added a commit to Jamkris/everything-claude-code that referenced this pull request May 13, 2026
…tch, subshell, +refspec)

Five additional review findings on top of the round-1 tokenizer fix.
Combined patch surface is small (one push branch, new switch branch,
exploded subshell handling); all six review issues are now closed.

P1 — --force --force-if-includes still destructive (Greptile, line 217):
  Previous logic treated --force-if-includes as a safety guarantee
  alongside --force-with-lease. Per git-scm.com/docs/git-push,
  --force-if-includes is a no-op WITHOUT --force-with-lease, so a
  combination of --force --force-if-includes is just --force. Push
  branch now treats only --force-with-lease as a lease, and reports
  force when --force / -f is present.

P2 — git switch destructive forms not detected (Greptile, line 234):
  Added a switch branch to isDestructiveGit covering:
    --discard-changes   (explicit discard)
    --force / -f        (ignore conflicts, overwrite)
    -C <branch>         (force-create, overwrites existing branch)

P0 — backtick + $(...) subshell bypass (CodeRabbit, line 64):
  Added explodeSubshells() that promotes `...` and $(...) contents
  to top-level segment separators. Run on both the SQL/dd regex
  input and the per-segment shell tokenizer input. Loops up to 4
  passes to catch a layer of nesting. Without this,
  `echo y | $(rm -rf /tmp)` slipped past the segment splitter
  because the destructive command lived inside a sub-expression.

P0 — +refspec force push (CodeRabbit, line 217):
  `git push origin +main`, `+refs/heads/main:refs/heads/main`, etc.
  force a non-fast-forward update of that specific ref. Push branch
  now also flags any positional arg starting with `+` that matches
  a refspec shape. Excludes bare `+` and numeric-only tokens.

P2 — missing --force --force-if-includes regression test
  (Greptile, line 1202): added.

Tests (+10 on top of the round-1 +10):
  Bypass-now-blocked:
    - git push --force --force-if-includes (force-if-includes is no-op
      without lease — bare force is still in effect)
    - git push origin +main (+refspec bare branch)
    - git push origin +refs/heads/main:refs/heads/main (+refspec full)
    - git switch --discard-changes
    - git switch --force
    - git switch -f (short form)
    - git switch -C (force-create)
    - echo y | `rm -rf /tmp` (backtick subshell)
    - echo y | $(rm -rf /tmp) (dollar-paren subshell)
  Still-allowed:
    - git switch feature (plain)

67/67 in gateguard-fact-force.test.js. 2380/2380 across the full
suite. yarn lint clean. All seven CI validators pass.

Refs affaan-m#1843.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants