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}