auto-update-daily-20260202
  1package main
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"io"
  7	"os"
  8	"regexp"
  9	"strings"
 10)
 11
 12// PreToolUseData represents the input from PreToolUse hook
 13type PreToolUseData struct {
 14	ToolName       string                 `json:"tool_name"`
 15	ToolInput      map[string]interface{} `json:"tool_input"`
 16	ConversationID string                 `json:"conversation_id"`
 17}
 18
 19// Check if a git push command uses explicit refspec (branch:branch)
 20func hasExplicitRefspec(command string) bool {
 21	// Match patterns like:
 22	// git push origin branch:branch
 23	// git push origin HEAD:branch
 24	// git push -u origin branch:branch
 25	// git push --force-with-lease origin branch:branch
 26	refspecPattern := regexp.MustCompile(`git\s+push\s+.*\s+\S+:\S+`)
 27	return refspecPattern.MatchString(command)
 28}
 29
 30// Check if this is a dangerous git add command
 31func isDangerousGitAdd(command string) bool {
 32	// Match patterns like:
 33	// git add -A
 34	// git add --all
 35	// git add .
 36	dangerousAddPatterns := []*regexp.Regexp{
 37		regexp.MustCompile(`(^|&&|\|\||;|\||\$\()\s*git\s+add\s+-A(\s|$)`),
 38		regexp.MustCompile(`(^|&&|\|\||;|\||\$\()\s*git\s+add\s+--all(\s|$)`),
 39		regexp.MustCompile(`(^|&&|\|\||;|\||\$\()\s*git\s+add\s+\.(\s|$)`),
 40	}
 41
 42	for _, pattern := range dangerousAddPatterns {
 43		if pattern.MatchString(command) {
 44			return true
 45		}
 46	}
 47	return false
 48}
 49
 50// Check if this is a git push command (not just the string "git push" inside arguments)
 51func isGitPush(command string) bool {
 52	// Match git push only when it appears as an actual command:
 53	// - At the start of the command
 54	// - After command separators: && || ; |
 55	// - After $( for command substitution
 56	// This avoids false positives on "git push" inside heredocs, strings, or commit messages
 57	gitPushPattern := regexp.MustCompile(`(^|&&|\|\||;|\||\$\()\s*git\s+push(\s|$)`)
 58	return gitPushPattern.MatchString(command)
 59}
 60
 61// Check if pushing to a protected branch without explicit refspec
 62func isPushToProtectedBranch(command string) bool {
 63	// These patterns indicate pushing to main/master without explicit refspec
 64	protectedBranches := []string{":main", ":master"}
 65	for _, branch := range protectedBranches {
 66		if strings.Contains(command, branch) {
 67			return true
 68		}
 69	}
 70	return false
 71}
 72
 73func main() {
 74	// Read input from stdin
 75	input, err := io.ReadAll(os.Stdin)
 76	if err != nil {
 77		fmt.Fprintf(os.Stderr, "[validate-git-push] Error reading stdin: %v\n", err)
 78		os.Exit(0) // Allow on error - don't block workflow
 79	}
 80
 81	if len(input) == 0 {
 82		os.Exit(0)
 83	}
 84
 85	var data PreToolUseData
 86	if err := json.Unmarshal(input, &data); err != nil {
 87		fmt.Fprintf(os.Stderr, "[validate-git-push] Error parsing JSON: %v\n", err)
 88		os.Exit(0) // Allow on error
 89	}
 90
 91	// Only check Bash tool
 92	if data.ToolName != "Bash" {
 93		os.Exit(0)
 94	}
 95
 96	// Get the command
 97	command, ok := data.ToolInput["command"].(string)
 98	if !ok || command == "" {
 99		os.Exit(0)
100	}
101
102	// Check for dangerous git add commands first
103	if isDangerousGitAdd(command) {
104		fmt.Fprintln(os.Stderr, "BLOCKED: Dangerous git add command detected!")
105		fmt.Fprintln(os.Stderr, "")
106		fmt.Fprintln(os.Stderr, "Commands like 'git add -A', 'git add --all', and 'git add .' can add unintended files.")
107		fmt.Fprintln(os.Stderr, "")
108		fmt.Fprintln(os.Stderr, "Use explicit file paths instead:")
109		fmt.Fprintln(os.Stderr, "  git add path/to/specific/file.txt")
110		fmt.Fprintln(os.Stderr, "  git add path/to/directory/")
111		fmt.Fprintln(os.Stderr, "  git add *.go  # for specific patterns")
112		fmt.Fprintln(os.Stderr, "")
113		fmt.Fprintf(os.Stderr, "Blocked command: %s\n", command)
114		os.Exit(2) // Non-zero exit blocks the tool
115	}
116
117	// Check if this is a git push command
118	if !isGitPush(command) {
119		os.Exit(0)
120	}
121
122	// Check if it has explicit refspec
123	if !hasExplicitRefspec(command) {
124		// Block the command - output error message to stderr and exit non-zero
125		fmt.Fprintln(os.Stderr, "BLOCKED: git push without explicit refspec detected!")
126		fmt.Fprintln(os.Stderr, "")
127		fmt.Fprintln(os.Stderr, "The command uses implicit branch tracking which can push to wrong branches.")
128		fmt.Fprintln(os.Stderr, "")
129		fmt.Fprintln(os.Stderr, "Use explicit refspec instead:")
130		fmt.Fprintln(os.Stderr, "  git push origin <branch>:<branch>")
131		fmt.Fprintln(os.Stderr, "  git push origin HEAD:<branch>")
132		fmt.Fprintln(os.Stderr, "")
133		fmt.Fprintf(os.Stderr, "Blocked command: %s\n", command)
134		os.Exit(2) // Non-zero exit blocks the tool
135	}
136
137	// Warn about pushing to protected branches (but allow it with explicit refspec)
138	if isPushToProtectedBranch(command) {
139		fmt.Fprintf(os.Stderr, "[validate-git-push] Warning: Pushing to protected branch (main/master)\n")
140	}
141
142	os.Exit(0) // Allow the command
143}