From 37af83aca408effa3bda76d2ef6eb5faadd55aed Mon Sep 17 00:00:00 2001 From: Chris Graf Date: Thu, 2 Jul 2026 07:35:03 -0500 Subject: [PATCH] Block `git push --mirror` in the pre-bash guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary Extend the force-push guard to catch `git push --mirror`, which force-overwrites and prunes every remote ref — remote-destructive even without `--force`. Follow-up to the `+` fix (#38). - add a dedicated `_GUARD_GIT_PUSH_MIRROR` guard and a `--mirror)` arm in the post-`push` argument scan - give it its own message (the force guard's `--force-with-lease` advice does not fit a mirror push): run it manually if a full mirror is truly intended - `--prune`/`--all` are left out of scope; `--mirror` is the clear remote-destructive gap Testing - new `agent-hook-bash-guards-test` cases: `git push --mirror`, `--mirror origin`, and `git -C repo push --mirror` are blocked with the mirror-specific message; normal pushes, `+refspec`, `--force-with-lease`, and `-u` are unaffected - verified red-green: removing the `--mirror)` arm lets `--mirror` through (exit 0) - full suite green (`./test/agentguard-test`: all suites 0 failed); `checkrun lint` clean --- bin/agent-hook-pre-bash | 11 +++++++++++ test/suites/agent-hook-bash-guards-test | 13 +++++++++++++ 2 files changed, 24 insertions(+) diff --git a/bin/agent-hook-pre-bash b/bin/agent-hook-pre-bash index 451d919..1a2fd5c 100755 --- a/bin/agent-hook-pre-bash +++ b/bin/agent-hook-pre-bash @@ -376,6 +376,7 @@ _hook_run_guards() { _GUARD_GIT_RESET_HARD=0 _GUARD_GIT_CLEAN_FORCE=0 _GUARD_GIT_PUSH_FORCE=0 + _GUARD_GIT_PUSH_MIRROR=0 _GUARD_TTY_REQUIRED=0 _GUARD_GIT_COMMIT=0 _GUARD_SL_COMMIT=0 @@ -508,6 +509,12 @@ _hook_run_guards() { _GUARD_GIT_PUSH_FORCE=1 break ;; + --mirror) + # --mirror force-overwrites AND prunes every remote ref, so it is + # remote-destructive even without --force. + _GUARD_GIT_PUSH_MIRROR=1 + break + ;; --*) ;; -?*) [[ "${word#-}" == *f* ]] && { _GUARD_GIT_PUSH_FORCE=1 @@ -746,6 +753,10 @@ fi [ "$_GUARD_GIT_PUSH_FORCE" -eq 1 ] && _hook_block 'git push --force rewrites remote history without a lease. Use --force-with-lease for reviewed restacks.' +# Block git push --mirror — force-overwrites and prunes every remote ref. +[ "$_GUARD_GIT_PUSH_MIRROR" -eq 1 ] && + _hook_block 'git push --mirror force-overwrites and prunes every remote ref. Run it manually if a full mirror is truly intended.' + # Block interactive TTY commands — they will silently hang the agent turn. [ "$_GUARD_TTY_REQUIRED" -eq 1 ] && _hook_block 'This command requires an interactive TTY and will hang. Use a non-interactive alternative (e.g. PAGER=cat, --no-pager, -batch).' diff --git a/test/suites/agent-hook-bash-guards-test b/test/suites/agent-hook-bash-guards-test index 2ab6a04..3f49950 100755 --- a/test/suites/agent-hook-bash-guards-test +++ b/test/suites/agent-hook-bash-guards-test @@ -174,6 +174,19 @@ _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" +# --mirror force-overwrites and prunes every remote ref — blocked, with its own +# (non-force) message so the advice fits. +_run_hook "$PRE_BASH" '{"tool_input":{"command":"git push --mirror"}}' +_assert_eq "git push --mirror: blocked" "2" "$HOOK_EXIT" +_assert_contains "git push --mirror: BLOCKED msg" "BLOCKED" "$HOOK_STDERR" +_assert_contains "git push --mirror: mirror-specific message" "--mirror force-overwrites" "$HOOK_STDERR" + +_run_hook "$PRE_BASH" '{"tool_input":{"command":"git push --mirror origin"}}' +_assert_eq "git push --mirror origin: blocked" "2" "$HOOK_EXIT" + +_run_hook "$PRE_BASH" '{"tool_input":{"command":"git -C /tmp/repo push --mirror"}}' +_assert_eq "git -C push --mirror: blocked" "2" "$HOOK_EXIT" + # echo of git push --force is prose _run_hook "$PRE_BASH" '{"tool_input":{"command":"echo git push --force"}}' _assert_eq "echo git push --force: allowed" "0" "$HOOK_EXIT"