diff --git a/src/agent/__tests__/hooks.test.ts b/src/agent/__tests__/hooks.test.ts index 05e42ee..e3a429a 100644 --- a/src/agent/__tests__/hooks.test.ts +++ b/src/agent/__tests__/hooks.test.ts @@ -109,6 +109,108 @@ describe("createDangerousCommandBlocker", () => { expect(result).toHaveProperty("decision", "block"); }); + test("blocks git push -f (short force 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 force 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", 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 git push --force-with-lease", 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 --force-with-lease origin main" }, + }), + undefined, + { signal: new AbortController().signal }, + ); + + expect(result).toHaveProperty("decision", "block"); + }); + + test("allows + inside a token (not a force refspec)", 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 tag+sign:main" }, + }), + undefined, + { signal: new AbortController().signal }, + ); + + expect(result).toEqual({ continue: true }); + }); + + test("allows -f outside a git push context", async () => { + const hook = createDangerousCommandBlocker(); + const callback = hook.hooks[0]; + + const result = await callback( + makeHookInput({ + hook_event_name: "PreToolUse", + tool_name: "Bash", + tool_input: { command: "ls -f /tmp" }, + }), + undefined, + { signal: new AbortController().signal }, + ); + + expect(result).toEqual({ continue: true }); + }); + 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 2d1460f..d9463d1 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+(?:\S+\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" },