Skip to content

Commit ed90360

Browse files
authored
Merge pull request #210 from notque/fix/hook-worktree-cwd-detection
fix(hooks): detect worktree CWD in branch-safety and unified-gate
2 parents 29ab879 + 4baacfd commit ed90360

2 files changed

Lines changed: 71 additions & 1 deletion

File tree

hooks/pretool-branch-safety.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import json
2424
import os
25+
import re
2526
import subprocess
2627
import sys
2728
import traceback
@@ -35,6 +36,30 @@
3536
_PROTECTED_BRANCHES = {"main", "master"}
3637

3738

39+
def _extract_effective_cwd(command: str, default_cwd: str | None) -> str | None:
40+
"""Extract the effective working directory from a command string.
41+
42+
Detects two patterns:
43+
- ``cd <path> && ...`` or ``cd <path> ; ...`` prefix
44+
- ``git -C <path> ...`` flag
45+
46+
Returns the extracted path if found, otherwise default_cwd.
47+
"""
48+
# Pattern 1: cd <path> && or cd <path> ;
49+
m = re.match(r'cd\s+(?:"([^"]+)"|(\S+))\s*(?:&&|;)', command.lstrip())
50+
if m:
51+
p = (m.group(1) or m.group(2) or "").strip()
52+
if p:
53+
return p
54+
55+
# Pattern 2: git -C <path>
56+
m = re.search(r'\bgit\s+-C\s+(?:"([^"]+)"|(\S+))', command)
57+
if m:
58+
return m.group(1) or m.group(2)
59+
60+
return default_cwd
61+
62+
3863
def _current_branch(cwd: str | None) -> str | None:
3964
"""Return the current git branch name, or None on error."""
4065
try:
@@ -74,7 +99,8 @@ def main() -> None:
7499
print("[branch-safety] Bypassed via BRANCH_SAFETY_BYPASS=1", file=sys.stderr)
75100
sys.exit(0)
76101

77-
cwd = event.get("cwd") or os.environ.get("CLAUDE_PROJECT_DIR")
102+
default_cwd = event.get("cwd") or os.environ.get("CLAUDE_PROJECT_DIR")
103+
cwd = _extract_effective_cwd(command, default_cwd)
78104
branch = _current_branch(cwd)
79105

80106
if debug:

hooks/pretool-unified-gate.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,44 @@ def check_gitignore_bypass(command: str) -> None:
266266
)
267267

268268

269+
def _extract_effective_cwd(command: str, default_cwd: str | None = None) -> str | None:
270+
"""Extract the effective working directory from a command string.
271+
272+
Detects two patterns:
273+
- ``cd <path> && ...`` or ``cd <path> ; ...`` prefix
274+
- ``git -C <path> ...`` flag
275+
276+
Returns the extracted path if found, otherwise default_cwd.
277+
"""
278+
m = re.match(r'cd\s+(?:"([^"]+)"|(\S+))\s*(?:&&|;)', command.lstrip())
279+
if m:
280+
p = (m.group(1) or m.group(2) or "").strip()
281+
if p:
282+
return p
283+
m = re.search(r'\bgit\s+-C\s+(?:"([^"]+)"|(\S+))', command)
284+
if m:
285+
return m.group(1) or m.group(2)
286+
return default_cwd
287+
288+
289+
def _is_worktree_on_feature_branch(cwd: str) -> bool:
290+
"""Return True if cwd is a worktree directory on a non-protected branch."""
291+
try:
292+
result = subprocess.run(
293+
["git", "branch", "--show-current"],
294+
capture_output=True,
295+
text=True,
296+
timeout=5,
297+
cwd=cwd,
298+
)
299+
if result.returncode == 0:
300+
branch = result.stdout.strip()
301+
return bool(branch) and branch not in {"main", "master"}
302+
except (subprocess.TimeoutExpired, OSError):
303+
pass
304+
return False
305+
306+
269307
def check_git_submission(command: str) -> None:
270308
"""Block raw git push, gh pr create, gh pr merge unless bypassed."""
271309
# Skills prefix blocked commands with CLAUDE_GATE_BYPASS=1 to pass through
@@ -274,6 +312,12 @@ def check_git_submission(command: str) -> None:
274312

275313
for pattern, skill_name, message in _GIT_SUBMISSION_PATTERNS:
276314
if pattern.search(command):
315+
# Allow git push from worktree directories on feature branches
316+
if pattern is _GIT_SUBMISSION_PATTERNS[0][0]: # git push pattern
317+
effective_cwd = _extract_effective_cwd(command)
318+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", "")
319+
if effective_cwd and effective_cwd != project_dir and _is_worktree_on_feature_branch(effective_cwd):
320+
return
277321
_block(f"[git-submission-gate] BLOCKED: {message}\n[fix-with-skill] {skill_name}")
278322

279323

0 commit comments

Comments
 (0)