#!/bin/bash
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
[ -z "$COMMAND" ] && exit 0
# git takes its own options BEFORE the verb, so `git -C /repo push --force` does
# exactly what the plain form does while `git\s+push` never sees it. Strip the
# options first and every rule below can assume the verb sits next to `git`.
CHECK=$(printf '%s' "$COMMAND" | sed -E ':a;
s/(^|[;&|[:space:]])git[[:space:]]+((-C|-c)[[:space:]]+[^[:space:]]+|--(git-dir|work-tree|namespace|exec-path)=[^[:space:]]+|--(no-pager|paginate|bare))[[:space:]]+/\1git /g; ta')
# [^;&|]* keeps each scan inside the push itself.
if printf '%s' "$CHECK" | grep -qE 'git\s+push\b[^;&|]*\b(main|master)\b'; then
echo "BLOCKED: Direct push to main" >&2; exit 2
fi
# Bundled short flags (`-uf`) are a force-push too.
if printf '%s' "$CHECK" | grep -qE 'git\s+push\b[^;&|]*(--force\b|--force-with-lease\b|[^A-Za-z0-9]-[a-zA-Z]*f\b)'; then
echo "BLOCKED: Force push" >&2; exit 2
fi
exit 0
Block destructive git when dirty BLOCK
#!/bin/bash
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
[ -z "$COMMAND" ] && exit 0
echo "$COMMAND" | grep -qE 'git\s+(checkout\s+--|reset\s+--hard|clean\s+-f)' || exit 0
DIRTY=$(git status --porcelain 2>/dev/null)
if [ -n "$DIRTY" ]; then
echo "BLOCKED: Uncommitted changes would be lost" >&2; exit 2
fi
exit 0
Block commits with conflict markers BLOCK
#!/bin/bash
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
echo "$COMMAND" | grep -qE 'git\s+commit' || exit 0
CONFLICTS=$(git diff --cached --name-only 2>/dev/null | while read f; do
[ -f "$f" ] && grep -lE '^(<{7}|={7}|>{7})' "$f" 2>/dev/null
done)
if [ -n "$CONFLICTS" ]; then
echo "BLOCKED: Conflict markers in staged files" >&2; exit 2
fi
exit 0
Auto-Approve Patterns
Auto-approve safe commands APPROVE
#!/bin/bash
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
[ -z "$COMMAND" ] && exit 0
if echo "$COMMAND" | grep -qE '^\s*(npm\s+test|cargo\s+test|go\s+test|pytest|make\s+test)'; then
echo '{"decision":"approve","reason":"Safe test command"}'
exit 0
fi
exit 0
Auto-approve read-only git APPROVE
#!/bin/bash
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
[ -z "$COMMAND" ] && exit 0
if echo "$COMMAND" | grep -qE '^\s*git\s+(status|log|diff|show|branch|remote|tag\s+-l)'; then
echo '{"decision":"approve","reason":"Read-only git"}'
fi
exit 0