Commit a02fdc9d40dd
Changed files (3)
dots
.config
claude
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 = {