flake-update-20260505
  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}