diff --git a/bin/agent-hook-pre-bash b/bin/agent-hook-pre-bash index 1a2fd5c..4124cdc 100755 --- a/bin/agent-hook-pre-bash +++ b/bin/agent-hook-pre-bash @@ -377,6 +377,7 @@ _hook_run_guards() { _GUARD_GIT_CLEAN_FORCE=0 _GUARD_GIT_PUSH_FORCE=0 _GUARD_GIT_PUSH_MIRROR=0 + _GUARD_GIT_PUSH_PRUNE=0 _GUARD_TTY_REQUIRED=0 _GUARD_GIT_COMMIT=0 _GUARD_SL_COMMIT=0 @@ -515,6 +516,13 @@ _hook_run_guards() { _GUARD_GIT_PUSH_MIRROR=1 break ;; + --prune) + # --prune deletes remote refs that have no local counterpart. + # (Bare --all is NOT guarded: it only pushes branches and is + # destructive only with --force, which is already caught above.) + _GUARD_GIT_PUSH_PRUNE=1 + break + ;; --*) ;; -?*) [[ "${word#-}" == *f* ]] && { _GUARD_GIT_PUSH_FORCE=1 @@ -757,6 +765,10 @@ fi [ "$_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 git push --prune — deletes remote refs with no local counterpart. +[ "$_GUARD_GIT_PUSH_PRUNE" -eq 1 ] && + _hook_block 'git push --prune deletes remote refs that have no local branch. Run it manually if the remote deletion is 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 3f49950..f1c2528 100755 --- a/test/suites/agent-hook-bash-guards-test +++ b/test/suites/agent-hook-bash-guards-test @@ -187,6 +187,17 @@ _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" +# --prune deletes remote refs with no local counterpart — blocked with its own message. +_run_hook "$PRE_BASH" '{"tool_input":{"command":"git push --prune origin refs/heads/*"}}' +_assert_eq "git push --prune: blocked" "2" "$HOOK_EXIT" +_assert_contains "git push --prune: prune-specific message" "--prune deletes remote refs" "$HOOK_STDERR" + +# Bare --all only pushes branches (destructive only with --force, already caught), +# so it must NOT be blocked on its own. +_run_hook "$PRE_BASH" '{"tool_input":{"command":"git push --all origin"}}' +_assert_eq "git push --all: allowed" "0" "$HOOK_EXIT" +_assert_not_contains "git push --all: no BLOCKED" "BLOCKED" "$HOOK_STDERR" + # 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"