main
1/**
2 * Pi Extension: Git Push Validation
3 *
4 * Validates git commands to prevent common mistakes:
5 * - Blocks git push without explicit refspec (branch:branch)
6 * - Blocks dangerous git add commands (-A, --all, .)
7 * - Warns about pushing to protected branches (main/master)
8 *
9 * This is a TypeScript migration of the Go-based claude-hooks-validate-git-push.
10 *
11 * Safety rules:
12 * 1. Always use explicit refspec: git push origin branch:branch
13 * 2. Never use git add -A, git add --all, or git add .
14 * 3. Use specific file paths for git add
15 */
16
17import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
18
19/**
20 * Check if a git push command uses explicit refspec (branch:branch)
21 */
22function hasExplicitRefspec(command: string): boolean {
23 // Match patterns like:
24 // git push origin branch:branch
25 // git push origin HEAD:branch
26 // git push -u origin branch:branch
27 // git push --force-with-lease origin branch:branch
28 const refspecPattern = /git\s+push\s+.*\s+\S+:\S+/;
29 return refspecPattern.test(command);
30}
31
32/**
33 * Check if this is a dangerous git add command
34 */
35function isDangerousGitAdd(command: string): boolean {
36 // Match patterns like:
37 // git add -A
38 // git add --all
39 // git add .
40 const dangerousAddPatterns = [
41 /(^|&&|\|\||;|\||\$\()\s*git\s+add\s+-A(\s|$)/,
42 /(^|&&|\|\||;|\||\$\()\s*git\s+add\s+--all(\s|$)/,
43 /(^|&&|\|\||;|\||\$\()\s*git\s+add\s+\.(\s|$)/,
44 ];
45
46 return dangerousAddPatterns.some((pattern) => pattern.test(command));
47}
48
49/**
50 * Check if this is a git push command (not just the string "git push" inside arguments)
51 */
52function isGitPush(command: string): boolean {
53 // Match git push only when it appears as an actual command:
54 // - At the start of the command
55 // - After command separators: && || ; |
56 // - After $( for command substitution
57 // This avoids false positives on "git push" inside heredocs, strings, or commit messages
58 const gitPushPattern = /(^|&&|\|\||;|\||\$\()\s*git\s+push(\s|$)/;
59 return gitPushPattern.test(command);
60}
61
62/**
63 * Check if pushing to a protected branch without explicit refspec
64 */
65function isPushToProtectedBranch(command: string): boolean {
66 // These patterns indicate pushing to main/master without explicit refspec
67 const protectedBranches = [":main", ":master"];
68 return protectedBranches.some((branch) => command.includes(branch));
69}
70
71export default function (pi: ExtensionAPI) {
72 // Pre-tool-use validation (can block)
73 pi.on("tool_call", async (event, ctx) => {
74 // Only check bash commands
75 if (event.toolName.toLowerCase() !== "bash") {
76 return undefined;
77 }
78
79 const command = event.input?.command;
80 if (!command || typeof command !== "string") {
81 return undefined;
82 }
83
84 // Check for dangerous git add commands first
85 if (isDangerousGitAdd(command)) {
86 const errorMessage = [
87 "BLOCKED: Dangerous git add command detected!",
88 "",
89 "Commands like 'git add -A', 'git add --all', and 'git add .' can add unintended files.",
90 "",
91 "Use explicit file paths instead:",
92 " git add path/to/specific/file.txt",
93 " git add path/to/directory/",
94 " git add *.go # for specific patterns",
95 "",
96 `Blocked command: ${command}`,
97 ].join("\n");
98
99 ctx.ui.notify(errorMessage, "error");
100
101 return {
102 block: true,
103 reason: errorMessage,
104 };
105 }
106
107 // Check if this is a git push command
108 if (!isGitPush(command)) {
109 return undefined;
110 }
111
112 // Check if it has explicit refspec
113 if (!hasExplicitRefspec(command)) {
114 const errorMessage = [
115 "BLOCKED: git push without explicit refspec detected!",
116 "",
117 "The command uses implicit branch tracking which can push to wrong branches.",
118 "",
119 "Use explicit refspec instead:",
120 " git push origin <branch>:<branch>",
121 " git push origin HEAD:<branch>",
122 "",
123 `Blocked command: ${command}`,
124 ].join("\n");
125
126 ctx.ui.notify(errorMessage, "error");
127
128 return {
129 block: true,
130 reason: errorMessage,
131 };
132 }
133
134 // Warn about pushing to protected branches (but allow it with explicit refspec)
135 if (isPushToProtectedBranch(command)) {
136 ctx.ui.notify(
137 "[validate-git-push] Warning: Pushing to protected branch (main/master)",
138 "warning"
139 );
140 }
141
142 return undefined; // Allow the command
143 });
144}