Skip to content

Instantly share code, notes, and snippets.

@efstathiosntonas
Last active January 16, 2026 18:52
Show Gist options
  • Select an option

  • Save efstathiosntonas/a9801a7449d2bab811d2c313280d09ec to your computer and use it in GitHub Desktop.

Select an option

Save efstathiosntonas/a9801a7449d2bab811d2c313280d09ec to your computer and use it in GitHub Desktop.

Prevent dangerous Git Operations Hook

Create .claude/hooks/block-git-operations.sh and register it in settings.json.

Configuration

Add to .claude/settings.json:

"hooks": {
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "command",
          "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-git-operations.sh"
        }
      ]
    }
  ]
}

Blocked Operations

Command Reason
git push --force / -f Destroys remote history
git reset --hard Discards uncommitted changes permanently
git clean -fd Deletes untracked files permanently
git checkout . Discards all unstaged changes
git stash drop/clear Permanently deletes stashes
git branch -D Force deletes without merge check
git rebase -i Interactive mode requires user input
git add -A / --all / . Too broad - be explicit about files
git *-abort Loses progress on cherry-pick/rebase
#!/bin/bash
# PreToolUse hook to block dangerous git operations
# Returns non-zero exit code to block, zero to allow
# Can output JSON with "decision" and "reason" fields
# Read the tool invocation from stdin
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
TOOL_INPUT=$(echo "$INPUT" | jq -r '.tool_input // empty')
# Only check Bash tool
if [[ "$TOOL_NAME" != "Bash" ]]; then
exit 0
fi
# Extract the command from tool input
COMMAND=$(echo "$TOOL_INPUT" | jq -r '.command // empty')
# Define blocked patterns with reasons
declare -A BLOCKED_PATTERNS=(
["git push --force"]="Force push can destroy remote history"
["git push -f "]="Force push can destroy remote history"
["git reset --hard"]="Hard reset discards uncommitted changes permanently"
["git clean -fd"]="Clean -fd deletes untracked files permanently"
["git checkout ."]="Discards all unstaged changes"
["git checkout -- ."]="Discards all unstaged changes"
["git stash drop"]="Permanently deletes stashed changes"
["git stash clear"]="Permanently deletes all stashes"
["git branch -D"]="Force deletes branch without merge check"
["git rebase -i"]="Interactive rebase requires user input"
["git add -A"]="Stages all files including untracked - be explicit"
["git add --all"]="Stages all files including untracked - be explicit"
["git add ."]="Stages everything in current directory - be explicit about files"
["git merge --no-ff"]="Use explicit merge strategies"
["git cherry-pick --abort"]="Aborting cherry-pick loses progress"
["git rebase --abort"]="Aborting rebase loses progress"
)
# Check each blocked pattern
for pattern in "${!BLOCKED_PATTERNS[@]}"; do
if [[ "$COMMAND" == *"$pattern"* ]]; then
REASON="${BLOCKED_PATTERNS[$pattern]}"
# Output JSON response with block decision
cat << EOF
{
"decision": "block",
"reason": "🚫 Blocked: $pattern - $REASON"
}
EOF
exit 2
fi
done
# Additional check: block any git command with --force or -f flag on push/reset
if [[ "$COMMAND" =~ git\ (push|reset).*(-f|--force) ]]; then
cat << EOF
{
"decision": "block",
"reason": "🚫 Blocked: Force flags on git push/reset are dangerous"
}
EOF
exit 2
fi
# Allow the operation
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment