main
1#!/usr/bin/env bun
2/**
3 * PreToolUse hook (Bash) — validate git commands for safety.
4 *
5 * Matches pi's validate-git-push.ts extension behaviour:
6 * • git push without explicit refspec → block
7 * • git add -A / --all / . → block
8 * • push to protected branches → warn (stderr)
9 *
10 * Handles command chaining: &&, ||, ;, |, $()
11 */
12
13import { readStdinJSON } from "./lib.ts";
14
15interface PreToolInput {
16 tool_name: string;
17 tool_input: { command?: string };
18}
19
20// Match git commands even after chaining operators
21const CMD_PREFIX = String.raw`(^|&&|\|\||;|\||\$\()\s*`;
22
23const DANGEROUS_ADD = [
24 new RegExp(CMD_PREFIX + String.raw`git\s+add\s+-A(\s|$)`),
25 new RegExp(CMD_PREFIX + String.raw`git\s+add\s+--all(\s|$)`),
26 new RegExp(CMD_PREFIX + String.raw`git\s+add\s+\.(\s|$)`),
27];
28
29const GIT_PUSH = new RegExp(CMD_PREFIX + String.raw`git\s+push(\s|$)`);
30const HAS_REFSPEC = /git\s+push\s+.*\s+\S+:\S+/;
31
32function isDangerousAdd(cmd: string): boolean {
33 return DANGEROUS_ADD.some((re) => re.test(cmd));
34}
35
36function isGitPush(cmd: string): boolean {
37 return GIT_PUSH.test(cmd);
38}
39
40function pushesToProtected(cmd: string): boolean {
41 return [":main", ":master"].some((b) => cmd.includes(b));
42}
43
44async function main() {
45 const data = await readStdinJSON<PreToolInput>();
46 if (!data) process.exit(0);
47
48 const cmd = data.tool_input?.command?.trim() || "";
49 if (!cmd) process.exit(0);
50
51 // ── git add safety ────────────────────────────────────────────
52 if (isDangerousAdd(cmd)) {
53 process.stdout.write(
54 JSON.stringify({
55 decision: "block",
56 reason: [
57 "BLOCKED: Dangerous git add command detected!",
58 "",
59 "Commands like 'git add -A', 'git add --all', and 'git add .' can add unintended files.",
60 "",
61 "Use explicit file paths instead:",
62 " git add path/to/specific/file.txt",
63 " git add path/to/directory/",
64 "",
65 `Blocked command: ${cmd}`,
66 ].join("\n"),
67 })
68 );
69 process.exit(0);
70 }
71
72 // ── git push safety ───────────────────────────────────────────
73 if (isGitPush(cmd)) {
74 if (!HAS_REFSPEC.test(cmd)) {
75 process.stdout.write(
76 JSON.stringify({
77 decision: "block",
78 reason: [
79 "BLOCKED: git push without explicit refspec!",
80 "",
81 "Implicit branch tracking can push to wrong branches.",
82 "",
83 "Use explicit refspec instead:",
84 " git push origin <branch>:<branch>",
85 " git push origin HEAD:<branch>",
86 "",
87 `Blocked command: ${cmd}`,
88 ].join("\n"),
89 })
90 );
91 process.exit(0);
92 }
93
94 if (pushesToProtected(cmd)) {
95 process.stderr.write(
96 "⚠️ Warning: pushing to protected branch (main/master)\n"
97 );
98 }
99 }
100
101 process.exit(0);
102}
103
104main();