Commit d5a3a6b0246c
Changed files (2)
dots
pi
agent
extensions
dots/pi/agent/extensions/claude-hooks.ts
@@ -124,26 +124,8 @@ export default function (pi: ExtensionAPI) {
sessionInitialized = false;
});
- // Pre-tool-use validation (can block)
- pi.on("tool_call", async (event, _ctx) => {
- // Run validate-git-push for bash commands
- if (
- event.toolName.toLowerCase() === "bash" &&
- binaryExists("claude-hooks-validate-git-push")
- ) {
- const jsonInput = toClaudePreToolUse(event);
- const result = await runHook("claude-hooks-validate-git-push", jsonInput);
-
- if (result.exitCode !== 0) {
- return {
- block: true,
- reason: result.stderr.trim() || "Command blocked by validate-git-push hook",
- };
- }
- }
-
- return undefined;
- });
+ // Pre-tool-use validation is now handled by validate-git-push.ts extension
+ // No need to call Go binary here anymore
// Post-tool-use capture
pi.on("tool_result", async (event, _ctx) => {
dots/pi/agent/extensions/validate-git-push.ts
@@ -0,0 +1,144 @@
+/**
+ * Pi Extension: Git Push Validation
+ *
+ * Validates git commands to prevent common mistakes:
+ * - Blocks git push without explicit refspec (branch:branch)
+ * - Blocks dangerous git add commands (-A, --all, .)
+ * - Warns about pushing to protected branches (main/master)
+ *
+ * This is a TypeScript migration of the Go-based claude-hooks-validate-git-push.
+ *
+ * Safety rules:
+ * 1. Always use explicit refspec: git push origin branch:branch
+ * 2. Never use git add -A, git add --all, or git add .
+ * 3. Use specific file paths for git add
+ */
+
+import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
+
+/**
+ * Check if a git push command uses explicit refspec (branch:branch)
+ */
+function hasExplicitRefspec(command: string): boolean {
+ // 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
+ const refspecPattern = /git\s+push\s+.*\s+\S+:\S+/;
+ return refspecPattern.test(command);
+}
+
+/**
+ * Check if this is a dangerous git add command
+ */
+function isDangerousGitAdd(command: string): boolean {
+ // Match patterns like:
+ // git add -A
+ // git add --all
+ // git add .
+ const dangerousAddPatterns = [
+ /(^|&&|\|\||;|\||\$\()\s*git\s+add\s+-A(\s|$)/,
+ /(^|&&|\|\||;|\||\$\()\s*git\s+add\s+--all(\s|$)/,
+ /(^|&&|\|\||;|\||\$\()\s*git\s+add\s+\.(\s|$)/,
+ ];
+
+ return dangerousAddPatterns.some((pattern) => pattern.test(command));
+}
+
+/**
+ * Check if this is a git push command (not just the string "git push" inside arguments)
+ */
+function isGitPush(command: string): boolean {
+ // Match git push only when it appears as an actual command:
+ // - At the start of the command
+ // - After command separators: && || ; |
+ // - After $( for command substitution
+ // This avoids false positives on "git push" inside heredocs, strings, or commit messages
+ const gitPushPattern = /(^|&&|\|\||;|\||\$\()\s*git\s+push(\s|$)/;
+ return gitPushPattern.test(command);
+}
+
+/**
+ * Check if pushing to a protected branch without explicit refspec
+ */
+function isPushToProtectedBranch(command: string): boolean {
+ // These patterns indicate pushing to main/master without explicit refspec
+ const protectedBranches = [":main", ":master"];
+ return protectedBranches.some((branch) => command.includes(branch));
+}
+
+export default function (pi: ExtensionAPI) {
+ // Pre-tool-use validation (can block)
+ pi.on("tool_call", async (event, ctx) => {
+ // Only check bash commands
+ if (event.toolName.toLowerCase() !== "bash") {
+ return undefined;
+ }
+
+ const command = event.input?.command;
+ if (!command || typeof command !== "string") {
+ return undefined;
+ }
+
+ // Check for dangerous git add commands first
+ if (isDangerousGitAdd(command)) {
+ const errorMessage = [
+ "BLOCKED: Dangerous git add command detected!",
+ "",
+ "Commands like 'git add -A', 'git add --all', and 'git add .' can add unintended files.",
+ "",
+ "Use explicit file paths instead:",
+ " git add path/to/specific/file.txt",
+ " git add path/to/directory/",
+ " git add *.go # for specific patterns",
+ "",
+ `Blocked command: ${command}`,
+ ].join("\n");
+
+ ctx.ui.notify(errorMessage, "error");
+
+ return {
+ block: true,
+ reason: errorMessage,
+ };
+ }
+
+ // Check if this is a git push command
+ if (!isGitPush(command)) {
+ return undefined;
+ }
+
+ // Check if it has explicit refspec
+ if (!hasExplicitRefspec(command)) {
+ const errorMessage = [
+ "BLOCKED: git push without explicit refspec detected!",
+ "",
+ "The command uses implicit branch tracking which can push to wrong branches.",
+ "",
+ "Use explicit refspec instead:",
+ " git push origin <branch>:<branch>",
+ " git push origin HEAD:<branch>",
+ "",
+ `Blocked command: ${command}`,
+ ].join("\n");
+
+ ctx.ui.notify(errorMessage, "error");
+
+ return {
+ block: true,
+ reason: errorMessage,
+ };
+ }
+
+ // Warn about pushing to protected branches (but allow it with explicit refspec)
+ if (isPushToProtectedBranch(command)) {
+ ctx.ui.notify(
+ "[validate-git-push] Warning: Pushing to protected branch (main/master)",
+ "warning"
+ );
+ }
+
+ return undefined; // Allow the command
+ });
+}