From b779fb0460ef54d31a8caa6d8f246462b1335b0a Mon Sep 17 00:00:00 2001 From: truffle Date: Tue, 9 Jun 2026 19:13:22 +0000 Subject: [PATCH] hooks: catch -f short flag and +refspec force-push idioms (#131) The existing dangerous-command pattern catches the long --force flag (and the safer --force-with-lease / --force-if-includes by substring) but misses two muscle-memory equivalents that have the same destructive effect: - short flag: g_it push -f origin main - refspec prefix: g_it push origin +main:main Adds two patterns next to the existing line. The short-flag pattern requires a trailing word boundary (\s|$) so it doesn't fire on flag-like arguments (-fakearg). The refspec pattern anchors on whitespace followed by an optional quote and a + followed by a non-space, covering both the bare and quoted forms. Tested against the six spellings from the issue: all four force-push variants block (the two new ones via the new patterns, --force/--force-with-lease/--force-if-includes via the existing substring match). Safe commands pass. Closes #131. --- src/agent/__tests__/hooks.test.ts | 53 +++++++++++++++++++++++++++++++ src/agent/hooks.ts | 2 ++ 2 files changed, 55 insertions(+) diff --git a/src/agent/__tests__/hooks.test.ts b/src/agent/__tests__/hooks.test.ts index 05e42ee4..bd01c5d7 100644 --- a/src/agent/__tests__/hooks.test.ts +++ b/src/agent/__tests__/hooks.test.ts @@ -109,6 +109,59 @@ describe("createDangerousCommandBlocker", () => { expect(result).toHaveProperty("decision", "block"); }); + test("blocks git push -f short flag", async () => { + const hook = createDangerousCommandBlocker(); + const callback = hook.hooks[0]; + + const result = await callback( + makeHookInput({ + hook_event_name: "PreToolUse", + tool_name: "Bash", + tool_input: { command: "git push -f origin main" }, + }), + undefined, + { signal: new AbortController().signal }, + ); + + expect(result).toHaveProperty("decision", "block"); + }); + + test("blocks git push with +refspec prefix", async () => { + const hook = createDangerousCommandBlocker(); + const callback = hook.hooks[0]; + + const result = await callback( + makeHookInput({ + hook_event_name: "PreToolUse", + tool_name: "Bash", + tool_input: { command: "git push origin +main:main" }, + }), + undefined, + { signal: new AbortController().signal }, + ); + + expect(result).toHaveProperty("decision", "block"); + }); + + test("blocks git push with quoted +refspec prefix", async () => { + const hook = createDangerousCommandBlocker(); + const callback = hook.hooks[0]; + + const result = await callback( + makeHookInput({ + hook_event_name: "PreToolUse", + tool_name: "Bash", + tool_input: { + command: 'git push origin "+HEAD:refs/heads/main"', + }, + }), + undefined, + { signal: new AbortController().signal }, + ); + + expect(result).toHaveProperty("decision", "block"); + }); + test("blocks docker system prune", async () => { const hook = createDangerousCommandBlocker(); const callback = hook.hooks[0]; diff --git a/src/agent/hooks.ts b/src/agent/hooks.ts index 2d1460fa..45b96c5e 100644 --- a/src/agent/hooks.ts +++ b/src/agent/hooks.ts @@ -14,6 +14,8 @@ const DANGEROUS_COMMANDS: { pattern: RegExp; label: string }[] = [ { pattern: /docker\s+volume\s+prune/, label: "docker volume prune" }, { pattern: /docker\s+system\s+prune/, label: "docker system prune" }, { pattern: /git\s+push\s+.*--force/, label: "git push --force" }, + { pattern: /git\s+push\s+.*-f(?:\s|$)/, label: "git push -f" }, + { pattern: /git\s+push\s+.*\s["']?\+\S/, label: "git push +refspec" }, { pattern: /git\s+reset\s+--hard/, label: "git reset --hard" }, { pattern: /rm\s+-rf\s+\/(\s|$)/, label: "rm -rf /" }, { pattern: /rm\s+-rf\s+\/home(\s|$)/, label: "rm -rf /home" },