Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions git-guards/scripts/git-permission-guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,14 @@ def _is_on_main_branch() -> bool:
2. Current branch name == "main" (fallback, git-based)

Returns False (fail-open) on any error.

GIT_GUARD_BRANCH_OVERRIDE: when set, returns (value == "main") without
touching git. Intended for CI/test use only — setting this in production
bypasses the branch guard entirely.
"""
override = os.environ.get("GIT_GUARD_BRANCH_OVERRIDE")
if override is not None:
return override == "main"
Comment thread
JacobPEvans marked this conversation as resolved.
try:
result = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
Expand Down
15 changes: 12 additions & 3 deletions git-guards/scripts/test_permission_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@
"""Tests for git-permission-guard.py ASK/DENY decisions."""

import json
import os
import subprocess
import sys
from pathlib import Path
from unittest.mock import patch

SCRIPT = Path(__file__).parent / "git-permission-guard.py"


def branch_override(branch: str):
return patch.dict(os.environ, {"GIT_GUARD_BRANCH_OVERRIDE": branch})


def run(cmd: str) -> dict:
inp = json.dumps({"tool_name": "Bash", "tool_input": {"command": cmd}})
result = subprocess.run(
Expand Down Expand Up @@ -73,7 +79,8 @@ def check(label: str, cmd: str, expected_decision: str) -> bool:
all_pass &= check("git push --force origin main", "git push --force origin main", "deny")

# git push --force to a feature branch - must ask (non-main target)
all_pass &= check("git push --force feature branch", "git push --force origin feature/my-branch", "ask")
with branch_override("feature"):
all_pass &= check("git push --force feature branch", "git push --force origin feature/my-branch", "ask")

# DENY: commit --no-verify
all_pass &= check("git commit --no-verify", "git commit -m msg --no-verify", "deny")
Expand All @@ -92,14 +99,16 @@ def check(label: str, cmd: str, expected_decision: str) -> bool:
all_pass &= check("git -C restore ask", "git -C /some/path restore file.txt", "ask")

# core.hooksPath precision: value containing the string should not trigger deny
all_pass &= check("hooksPath in value only", "git -c some.key=echo-core.hooksPath commit -m test", "silent_allow")
with branch_override("feature"):
all_pass &= check("hooksPath in value only", "git -c some.key=echo-core.hooksPath commit -m test", "silent_allow")

# Fallback bypass detection: unrecognised global option before -c breaks loop early
all_pass &= check("--no-pager before -c hooksPath", "git --no-pager -c core.hooksPath=/dev/null commit -m msg", "deny")
# Fallback also fires when loop parsed a prior -c but broke before a second -c hooksPath
all_pass &= check("valid -c then --bare then -c hooksPath", "git -c user.name=test --bare -c core.hooksPath=/dev/null commit -m msg", "deny")
# False positive guard: commit message containing the bypass pattern as a substring must not deny
all_pass &= check("hooksPath in commit message", 'git -c user.name=test commit -m "allow -c core.hooksPath bypass example"', "silent_allow")
with branch_override("feature"):
all_pass &= check("hooksPath in commit message", 'git -c user.name=test commit -m "allow -c core.hooksPath bypass example"', "silent_allow")

print()
print("ALL TESTS PASSED" if all_pass else "SOME TESTS FAILED")
Expand Down
Loading