Commit a02fdc9d40dd

Vincent Demeester <vincent@sbr.pm>
2026-01-28 17:09:26
feat(claude-hooks): add PreToolUse hook to validate git push refspecs
Add a new hook that intercepts git push commands before execution and blocks any push that doesn't use explicit branch:branch refspec syntax. This prevents accidental pushes to wrong branches when tracking is misconfigured. - Add validate-git-push command that checks for explicit refspecs - Configure PreToolUse hook in settings.json for Bash commands - Exit code 2 blocks the command with helpful error message Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 2faaee5
Changed files (3)
dots
.config
tools
claude-hooks
cmd
validate-git-push
dots/.config/claude/settings.json
@@ -5,10 +5,22 @@
       "args": ["stdio"]
     },
     "playwright": {
-      "command": "playwright-mcp"
+      "command": "mcp-server-playwright",
+      "args": ["--browser", "google-chrome-stable"]
     }
   },
   "hooks": {
+    "PreToolUse": [
+      {
+        "matcher": "Bash",
+        "hooks": [
+          {
+            "type": "command",
+            "command": "claude-hooks-validate-git-push"
+          }
+        ]
+      }
+    ],
     "SessionStart": [
       {
         "hooks": [
tools/claude-hooks/cmd/validate-git-push/main.go
@@ -0,0 +1,102 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"os"
+	"regexp"
+	"strings"
+)
+
+// PreToolUseData represents the input from PreToolUse hook
+type PreToolUseData struct {
+	ToolName       string                 `json:"tool_name"`
+	ToolInput      map[string]interface{} `json:"tool_input"`
+	ConversationID string                 `json:"conversation_id"`
+}
+
+// Check if a git push command uses explicit refspec (branch:branch)
+func hasExplicitRefspec(command string) bool {
+	// Match patterns like:
+	// git push origin branch:branch
+	// git push origin HEAD:branch
+	// git push -u origin branch:branch
+	// git push --force-with-lease origin branch:branch
+	refspecPattern := regexp.MustCompile(`git\s+push\s+.*\s+\S+:\S+`)
+	return refspecPattern.MatchString(command)
+}
+
+// Check if this is a git push command
+func isGitPush(command string) bool {
+	return strings.Contains(command, "git push") || strings.Contains(command, "git push")
+}
+
+// Check if pushing to a protected branch without explicit refspec
+func isPushToProtectedBranch(command string) bool {
+	// These patterns indicate pushing to main/master without explicit refspec
+	protectedBranches := []string{":main", ":master"}
+	for _, branch := range protectedBranches {
+		if strings.Contains(command, branch) {
+			return true
+		}
+	}
+	return false
+}
+
+func main() {
+	// Read input from stdin
+	input, err := io.ReadAll(os.Stdin)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "[validate-git-push] Error reading stdin: %v\n", err)
+		os.Exit(0) // Allow on error - don't block workflow
+	}
+
+	if len(input) == 0 {
+		os.Exit(0)
+	}
+
+	var data PreToolUseData
+	if err := json.Unmarshal(input, &data); err != nil {
+		fmt.Fprintf(os.Stderr, "[validate-git-push] Error parsing JSON: %v\n", err)
+		os.Exit(0) // Allow on error
+	}
+
+	// Only check Bash tool
+	if data.ToolName != "Bash" {
+		os.Exit(0)
+	}
+
+	// Get the command
+	command, ok := data.ToolInput["command"].(string)
+	if !ok || command == "" {
+		os.Exit(0)
+	}
+
+	// Check if this is a git push command
+	if !isGitPush(command) {
+		os.Exit(0)
+	}
+
+	// Check if it has explicit refspec
+	if !hasExplicitRefspec(command) {
+		// Block the command - output error message and exit non-zero
+		fmt.Println("BLOCKED: git push without explicit refspec detected!")
+		fmt.Println("")
+		fmt.Println("The command uses implicit branch tracking which can push to wrong branches.")
+		fmt.Println("")
+		fmt.Println("Use explicit refspec instead:")
+		fmt.Println("  git push origin <branch>:<branch>")
+		fmt.Println("  git push origin HEAD:<branch>")
+		fmt.Println("")
+		fmt.Printf("Blocked command: %s\n", command)
+		os.Exit(2) // Non-zero exit blocks the tool
+	}
+
+	// Warn about pushing to protected branches (but allow it with explicit refspec)
+	if isPushToProtectedBranch(command) {
+		fmt.Fprintf(os.Stderr, "[validate-git-push] Warning: Pushing to protected branch (main/master)\n")
+	}
+
+	os.Exit(0) // Allow the command
+}
tools/claude-hooks/default.nix
@@ -18,6 +18,7 @@ buildGoModule {
     "cmd/validate-docs"
     "cmd/save-session"
     "cmd/session-stats"
+    "cmd/validate-git-push"
   ];
 
   # Rename binaries to have consistent prefix
@@ -28,6 +29,7 @@ buildGoModule {
     mv $out/bin/validate-docs $out/bin/claude-hooks-validate-docs
     mv $out/bin/save-session $out/bin/claude-hooks-save-session
     mv $out/bin/session-stats $out/bin/claude-hooks-session-stats
+    mv $out/bin/validate-git-push $out/bin/claude-hooks-validate-git-push
   '';
 
   meta = {