#!/usr/bin/env bash # push-guard.sh — PreToolUse (Bash|Monitor): refuse `git push` in manual # push mode (BDR-111). Manual mode = `gitflow.autopush` reads false (or is # unparseable or unreadable: fail closed) in the payload cwd or in any literal -C / cd dir # the command names; outside a repo `git config` reads global/system. The # mode is read by the sourced lib verb gitflow_push_mode (single reader). # # Deny form: JSON on stdout, exit 0 (hookSpecificOutput.permissionDecision # = "deny"). Silent in auto mode and on every non-push command. The guard # sees the command TEXT only. Once a push is detected an EXIT trap emits a # static deny (exit 0) unless a decision was recorded: internal error = # push refused. jq, cat, grep, sed, sort or head missing: one stderr # warning, allow (sibling hooks; PATH is not command-controlled). # # DENIED beyond a manual-mode push: a cd/-C dir token mixing quoted and # unquoted parts ("/m"/x"/y", a/'../b'); a cd argument touching a closing # quote followed by another quote on the line (bash -c 'cd /x' && bash -c # 'git push' reads as one mixed token, accepted, fail closed); a payload jq # cannot parse whose raw text (JSON escapes folded) looks like a push: static # deny, mode-blind, so a description naming a push also denies there. # ARGV CHECK: once the text looks like a push, the command is split into # simple commands (quotes, escapes, `;` `&&` `||` `|` `(` `)` backticks, # heredoc bodies dropped) and the push must sit in one of them: argv[0] is # git (or */git) and its first non-option word is push or send-pack. A # command whose argv[0] is a plain program (grep, cat, echo, sed, rg...) or a # git subcommand other than push (log -S "git push", config --get # push.default, add push.sh, help push, stash push, commit -m "git push") # is proven not a push and allowed. `bash|sh|zsh -c