From 0e74a9dfe836904a3cd2e9c7d56a9e9c99bef044 Mon Sep 17 00:00:00 2001 From: truffle Date: Wed, 13 May 2026 02:09:20 +0000 Subject: [PATCH] hooks: cover -f and +refspec force-push spellings The dangerous-command blocker catches `--force` (including the `--force-with-lease` and `--force-if-includes` superstring forms) but misses two equivalent spellings: the short flag `-f`, and the refspec prefix `+:` that tells the remote to accept a non-fast-forward update. Adds two patterns. The `-f` form uses a token-walk `(?:\\S+\\s+)*` + boundary lookahead so substrings like `--force-if-includes` and `-fwoo` don't trip it. The `+refspec` form requires whitespace (or a wrapping quote) immediately before the `+`, so a token like `tag+sign:main` is not a false positive. Closes #131. --- src/agent/__tests__/hooks.test.ts | 102 ++++++++++++++++++++++++++++++ src/agent/hooks.ts | 2 + 2 files changed, 104 insertions(+) diff --git a/src/agent/__tests__/hooks.test.ts b/src/agent/__tests__/hooks.test.ts index 05e42ee4..e3a429a0 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 2d1460fa..d9463d1c 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" },