From 56bec211922dd550e773d06ff21bdabe341e8fee Mon Sep 17 00:00:00 2001 From: Chris Graf Date: Thu, 2 Jul 2026 07:00:32 -0500 Subject: [PATCH] Block `+` force pushes in the pre-bash guard Summary Close a bypass in the `git push --force` guard: a refspec that leads with `+` forces the remote update without `--force`/`-f`. `agent-hook-pre-bash` blocked `git push --force` and `-f`, but its per-argument scan only looked at flags. `git push origin +main`, `+HEAD:main`, or `+refs/heads/x:y` all force-overwrite the remote ref and slipped through unguarded. - add a `+?*)` arm to the post-`push` argument loop: a positional refspec starting with `+` (the force marker) sets the force-push guard. A remote name can't start with `+`, and a leading `+` on a refspec always means force, so there is no legitimate non-force arg this catches. - `--force-with-lease` (and `=ref`) stay allowed as before Known, pre-existing gaps left as-is: `git push origin -- +main` (the scan stops at `--`) and `--mirror`; neither is a common agent form. Testing - new `agent-hook-bash-guards-test` cases: `+main`, `+HEAD:main`, and `+refs/heads/x:y` are blocked; plain `origin main` and a non-force `HEAD:main` refspec stay allowed - verified red-green: removing the `+?*)` arm lets `+main` push through (exit 0 instead of blocked) - full suite green (`./test/agentguard-test`: all suites 0 failed); `checkrun lint` clean --- bin/agent-hook-pre-bash | 7 +++++++ test/suites/agent-hook-bash-guards-test | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/bin/agent-hook-pre-bash b/bin/agent-hook-pre-bash index adb0e1e..451d919 100755 --- a/bin/agent-hook-pre-bash +++ b/bin/agent-hook-pre-bash @@ -513,6 +513,13 @@ _hook_run_guards() { _GUARD_GIT_PUSH_FORCE=1 break } ;; + +?*) + # A refspec that leads with `+` (e.g. `+main`, `+HEAD:main`, + # `+refs/heads/x:y`) forces the push without `--force`/`-f`, so it + # must be caught too. --force-with-lease stays allowed above. + _GUARD_GIT_PUSH_FORCE=1 + break + ;; esac ((i++)) done diff --git a/test/suites/agent-hook-bash-guards-test b/test/suites/agent-hook-bash-guards-test index e0e91cf..2ab6a04 100755 --- a/test/suites/agent-hook-bash-guards-test +++ b/test/suites/agent-hook-bash-guards-test @@ -150,11 +150,27 @@ _assert_not_contains "git push --force-with-lease=ref: no BLOCKED" "BLOCKED" "$H _run_hook "$PRE_BASH" '{"tool_input":{"command":"git -C /tmp/repo push -f"}}' _assert_eq "git -C push -f: blocked" "2" "$HOOK_EXIT" +# `+` forces the push without --force/-f and must also be blocked. +_run_hook "$PRE_BASH" '{"tool_input":{"command":"git push origin +main"}}' +_assert_eq "git push origin +main: blocked" "2" "$HOOK_EXIT" +_assert_contains "git push origin +main: BLOCKED msg" "BLOCKED" "$HOOK_STDERR" + +_run_hook "$PRE_BASH" '{"tool_input":{"command":"git push origin +HEAD:main"}}' +_assert_eq "git push origin +HEAD:main: blocked" "2" "$HOOK_EXIT" + +_run_hook "$PRE_BASH" '{"tool_input":{"command":"git push origin +refs/heads/x:refs/heads/y"}}' +_assert_eq "git push origin +refspec: blocked" "2" "$HOOK_EXIT" + # normal push is fine _run_hook "$PRE_BASH" '{"tool_input":{"command":"git push origin main"}}' _assert_eq "git push (no force): allowed" "0" "$HOOK_EXIT" _assert_not_contains "git push (no force): no BLOCKED" "BLOCKED" "$HOOK_STDERR" +# a non-force src:dst refspec (no leading +) must NOT be flagged as force +_run_hook "$PRE_BASH" '{"tool_input":{"command":"git push origin HEAD:main"}}' +_assert_eq "git push src:dst (no force): allowed" "0" "$HOOK_EXIT" +_assert_not_contains "git push src:dst: no BLOCKED" "BLOCKED" "$HOOK_STDERR" + _run_hook "$PRE_BASH" '{"tool_input":{"command":"git push -u origin feat/foo"}}' _assert_eq "git push -u: allowed" "0" "$HOOK_EXIT"