Skip to content

refactor(git): replace bash+jq hook scripts with Python - #16

Merged
cblecker merged 1 commit into
mainfrom
worktree-cozy-gliding-eich
Feb 28, 2026
Merged

refactor(git): replace bash+jq hook scripts with Python#16
cblecker merged 1 commit into
mainfrom
worktree-cozy-gliding-eich

Conversation

@cblecker

@cblecker cblecker commented Feb 28, 2026

Copy link
Copy Markdown
Owner

Summary

  • Replace jq-dependent bash scripts with Python 3 stdlib equivalents for better portability (jq is not installed by default on macOS)
  • All 7 routing categories preserved with identical behavior
  • Fork detection extracted from inline jq one-liner in hooks.json into standalone script

Changes

  • git/scripts/git-bash-router.py (new) — Python 3 replacement for git-bash-router.sh, replicating all routing logic using json, re, and subprocess modules
  • git/scripts/fork-detector.py (new) — Extracted inline jq fork detection from hooks.json into standalone Python script
  • git/scripts/git-bash-router.sh (deleted) — Old bash+jq router
  • git/hooks/hooks.json — Updated command references to .py scripts
  • git/CLAUDE.md — Updated architecture, performance (~30-60ms), and technical notes
  • git/hooks/README.md — Updated architecture, test commands, and technical reference

Testing

All 14 test cases pass:

  • Skill enforcement: git commit, git checkout -b, gh pr create → deny
  • Override bypass: GIT_WORKFLOWS_OVERRIDE=1 prefix → null (passthrough)
  • Safety checks: force push mainline → deny, force push feature → ask
  • Warnings: git reset --hard, git clean -f, git rebase main → additionalContext
  • Passthrough: git status → null
  • Error handling: invalid JSON → null (graceful degradation)
  • Fork detector: no upstream → null, invalid JSON → null
  • Plugin validation: passed
  • Markdown linting: 0 errors

Summary by CodeRabbit

  • Refactor

    • Git hook routing system reimplemented with updated command pattern matching and safety check enforcement.
    • Added dedicated fork-detection script to validate PR creation.
  • Documentation

    • Updated architecture documentation and hook configuration examples to reflect implementation changes.
  • Chores

    • Updated hook script paths and runtime dependencies.

Replace jq-dependent bash scripts with Python 3 stdlib equivalents
to improve portability (jq is not installed by default on macOS).

- Create git-bash-router.py replicating all 7 routing categories
- Extract inline jq fork detection into fork-detector.py
- Delete git-bash-router.sh
- Update hooks.json command references
- Update CLAUDE.md and hooks/README.md documentation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Feb 28, 2026

Copy link
Copy Markdown

Walkthrough

This pull request migrates the Git hooks router implementation from a Bash/jq-based script to Python, introduces a new fork-detection script, and updates related documentation and configuration. The router enforces skill-based access controls and safety checks for Git commands with an override mechanism.

Changes

Cohort / File(s) Summary
Documentation Updates
git/CLAUDE.md, git/hooks/README.md
Updated descriptions, architecture diagrams, and examples to reflect the Python-based router implementation (re.search() instead of jq), revised performance metrics, and adjusted tool path references from Bash to Python scripts.
Configuration Migration
git/hooks/hooks.json
Replaced hook command references from scripts/git-bash-router.sh and inline jq fork detection to scripts/git-bash-router.py and scripts/fork-detector.py respectively.
New Python Router
git/scripts/git-bash-router.py
New Python implementation (144 lines) replacing the Bash router; provides command pattern matching via regex, mainline branch detection, and conditional enforcement of safety checks and skill-based access controls (git push, commit, reset, clean, rebase, branch creation, gh pr create) with GIT_WORKFLOWS_OVERRIDE support.
New Fork Detector
git/scripts/fork-detector.py
New Python script (49 lines) for pre-tool-use hook validation; reads JSON from stdin, extracts upstream remote owner, and warns if PR owner differs from upstream owner.
Removed Bash Router
git/scripts/git-bash-router.sh
Deleted entire file (93 lines) containing the legacy pure jq-based router and command matching logic.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 A Bash script hops away, now Python takes the stage,
With regex matches swift and fork-checks all the rage,
The router's reborn in modern tongue so clean,
Safety checks enforced on commits unforeseen!
From jq pipes to Python calls—a hop to newer days!

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor(git): replace bash+jq hook scripts with Python' accurately summarizes the main change—replacing Bash and jq-based routing scripts with Python equivalents across the git hooks.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch worktree-cozy-gliding-eich

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

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
git/scripts/fork-detector.py (2)

14-17: Add timeout to subprocess call for consistency.

The subprocess.run call here lacks a timeout parameter, unlike the similar call in git-bash-router.py (Line 19) which uses timeout=5. If the git remote get-url command hangs (e.g., due to network issues with some remote configurations), this could cause the hook to block indefinitely.

Proposed fix
     result = subprocess.run(
         ["git", "remote", "get-url", "upstream"],
-        capture_output=True, text=True
+        capture_output=True, text=True, timeout=5
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git/scripts/fork-detector.py` around lines 14 - 17, The subprocess.run call
that executes ["git","remote","get-url","upstream"] (assigned to result) must
include a timeout to avoid hanging; update the subprocess.run invocation in
fork-detector.py to pass timeout=5 (matching git-bash-router.py) and keep
capture_output=True, text=True so the call fails fast on network hangs and
raises a TimeoutExpired if exceeded.

28-36: Handle empty pr_owner to avoid false warnings.

If tool_input.owner is missing or empty, pr_owner will be an empty string. Comparing an empty string against a valid upstream_owner will always trigger the warning, even though the issue isn't a fork mismatch but rather missing input data.

Proposed fix
     upstream_owner = match.group("owner")
     pr_owner = data.get("tool_input", {}).get("owner", "")
 
+    if not pr_owner:
+        return None  # No owner specified, can't validate
+
     if pr_owner != upstream_owner:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git/scripts/fork-detector.py` around lines 28 - 36, The current comparison
between pr_owner and upstream_owner triggers false fork warnings when
tool_input.owner is missing or empty; update the logic around pr_owner and the
conditional that returns the warning (the block referencing pr_owner and
upstream_owner) to first detect and handle a missing/empty pr_owner (e.g., if
not pr_owner) by returning a clear non-fork result or a different
hookSpecificOutput indicating missing input, and only perform the pr_owner !=
upstream_owner fork check when pr_owner is present; ensure you update the branch
that currently returns the "WARNING: Fork detected..." message to only run when
pr_owner is non-empty.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@git/scripts/git-bash-router.py`:
- Around line 14-21: The code may leave mainline empty when
CLAUDE_MAINLINE_BRANCH is unset and the detect script fails; update the mainline
detection logic (the block that calls detect-mainline.sh and sets variable
mainline) to validate that mainline is non-empty and, if empty, set a sensible
default like "main" (or skip constructing regexes) before any use in
pushing_mainline() and the rebase check; ensure all regex constructions and
matches in functions pushing_mainline() and the rebase check use this validated
non-empty mainline to avoid unintended matches.

---

Nitpick comments:
In `@git/scripts/fork-detector.py`:
- Around line 14-17: The subprocess.run call that executes
["git","remote","get-url","upstream"] (assigned to result) must include a
timeout to avoid hanging; update the subprocess.run invocation in
fork-detector.py to pass timeout=5 (matching git-bash-router.py) and keep
capture_output=True, text=True so the call fails fast on network hangs and
raises a TimeoutExpired if exceeded.
- Around line 28-36: The current comparison between pr_owner and upstream_owner
triggers false fork warnings when tool_input.owner is missing or empty; update
the logic around pr_owner and the conditional that returns the warning (the
block referencing pr_owner and upstream_owner) to first detect and handle a
missing/empty pr_owner (e.g., if not pr_owner) by returning a clear non-fork
result or a different hookSpecificOutput indicating missing input, and only
perform the pr_owner != upstream_owner fork check when pr_owner is present;
ensure you update the branch that currently returns the "WARNING: Fork
detected..." message to only run when pr_owner is non-empty.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d71962 and 1935c16.

📒 Files selected for processing (6)
  • git/CLAUDE.md
  • git/hooks/README.md
  • git/hooks/hooks.json
  • git/scripts/fork-detector.py
  • git/scripts/git-bash-router.py
  • git/scripts/git-bash-router.sh
💤 Files with no reviewable changes (1)
  • git/scripts/git-bash-router.sh

Comment on lines +14 to +21
if not mainline:
plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT", "")
detect_script = os.path.join(plugin_root, "scripts", "detect-mainline.sh")
result = subprocess.run(
["bash", detect_script],
capture_output=True, text=True, timeout=5
)
mainline = result.stdout.strip()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Empty mainline can cause regex patterns to match unintended content.

If CLAUDE_MAINLINE_BRANCH is unset and the detect script fails (e.g., script not found, non-git directory), mainline remains an empty string. The patterns in pushing_mainline() (Line 41-43) and the rebase check (Line 92) would then match any whitespace followed by end-of-string or more whitespace, potentially causing false positives.

Consider validating that mainline is non-empty before using it in patterns, or falling back to a sensible default like "main".

Proposed fix
     if not mainline:
         plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT", "")
         detect_script = os.path.join(plugin_root, "scripts", "detect-mainline.sh")
         result = subprocess.run(
             ["bash", detect_script],
             capture_output=True, text=True, timeout=5
         )
         mainline = result.stdout.strip()
+    if not mainline:
+        mainline = "main"  # Fallback default
🧰 Tools
🪛 Ruff (0.15.2)

[error] 17-17: subprocess call: check for execution of untrusted input

(S603)


[error] 18-18: Starting a process with a partial executable path

(S607)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git/scripts/git-bash-router.py` around lines 14 - 21, The code may leave
mainline empty when CLAUDE_MAINLINE_BRANCH is unset and the detect script fails;
update the mainline detection logic (the block that calls detect-mainline.sh and
sets variable mainline) to validate that mainline is non-empty and, if empty,
set a sensible default like "main" (or skip constructing regexes) before any use
in pushing_mainline() and the rebase check; ensure all regex constructions and
matches in functions pushing_mainline() and the rebase check use this validated
non-empty mainline to avoid unintended matches.

@cblecker
cblecker merged commit e2f3d17 into main Feb 28, 2026
9 checks passed
@cblecker
cblecker deleted the worktree-cozy-gliding-eich branch February 28, 2026 00:48
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.

1 participant