Files
dotclaude/hooks/block-dangerous-commands.sh
T
Poshan Pandey 491a45dd43 Add dotclaude configuration files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:16:27 -07:00

137 lines
6.7 KiB
Bash

#!/bin/bash
# Blocks dangerous shell commands: push to main, force push, destructive operations.
# Used as a PreToolUse hook for Bash operations.
# Exit 2 = block the action. Exit 0 = allow.
# Requires jq for JSON parsing — fail closed if missing
if ! command -v jq >/dev/null 2>&1; then
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"jq is required for command protection hooks but is not installed.\"}}"
exit 2
fi
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if [ -z "$COMMAND" ]; then
exit 0
fi
deny() {
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"$1\"}}"
exit 2
}
# ──────────────────────────────────────────────
# Git protections
# ──────────────────────────────────────────────
# Check if the command contains git push (handles chaining with &&, ;, |, subshells)
if echo "$COMMAND" | grep -qE '(^|[;&|()]+[[:space:]]*)git[[:space:]]+push'; then
# Block push to main or master
if echo "$COMMAND" | grep -qE 'git[[:space:]]+push.*(origin[[:space:]]+|:)(main|master)\b'; then
deny "Blocked: cannot push directly to main/master. Use a feature branch and create a PR."
fi
# Block bare "git push" when on main/master
if echo "$COMMAND" | grep -qE 'git[[:space:]]+push[[:space:]]*($|[;&|])'; then
CURRENT_BRANCH=$(git branch --show-current 2>/dev/null)
if [ "$CURRENT_BRANCH" = "main" ] || [ "$CURRENT_BRANCH" = "master" ]; then
deny "Blocked: you are on $CURRENT_BRANCH. Use a feature branch and create a PR."
fi
fi
# Block force push (allow --force-with-lease)
if echo "$COMMAND" | grep -qE 'git[[:space:]]+push.*(-[a-zA-Z]*f|--force)([[:space:]]|$)' && ! echo "$COMMAND" | grep -q '\-\-force-with-lease'; then
deny "Blocked: force push is not allowed. Use --force-with-lease if you need to overwrite remote."
fi
fi
# ──────────────────────────────────────────────
# Destructive filesystem operations
# ──────────────────────────────────────────────
# Block rm -rf on root, home, or broad paths
if echo "$COMMAND" | grep -qE 'rm[[:space:]]+-[a-zA-Z]*r[a-zA-Z]*f[[:space:]]+(\/|~|\$HOME|\.\.\/\.\.)'; then
deny "Blocked: recursive force-delete on root/home/parent paths. Specify a safe target directory."
fi
# Block rm -rf / or rm -rf /* or rm -rf ~
if echo "$COMMAND" | grep -qE 'rm[[:space:]]+-[a-zA-Z]*r.*[[:space:]]+(\/[[:space:]]|\/\*|\/$|~\/?\*?[[:space:]]|~\/?\*?$)'; then
deny "Blocked: recursive delete targeting root or home directory."
fi
# ──────────────────────────────────────────────
# Dangerous database operations
# ──────────────────────────────────────────────
# Block DROP TABLE/DATABASE without safeguards
if echo "$COMMAND" | grep -qiE 'DROP[[:space:]]+(TABLE|DATABASE|SCHEMA)[[:space:]]'; then
deny "Blocked: DROP TABLE/DATABASE/SCHEMA detected. This is destructive and irreversible. Run manually if intended."
fi
# Block DELETE FROM without WHERE
if echo "$COMMAND" | grep -qiE 'DELETE[[:space:]]+FROM[[:space:]]+[a-zA-Z_]+[[:space:]]*($|;)' && ! echo "$COMMAND" | grep -qiE 'WHERE'; then
deny "Blocked: DELETE FROM without WHERE clause would delete all rows. Add a WHERE clause."
fi
# Block TRUNCATE TABLE
if echo "$COMMAND" | grep -qiE 'TRUNCATE[[:space:]]+TABLE'; then
deny "Blocked: TRUNCATE TABLE detected. This is destructive and irreversible. Run manually if intended."
fi
# ──────────────────────────────────────────────
# Dangerous system commands
# ──────────────────────────────────────────────
# Block chmod 777
if echo "$COMMAND" | grep -qE 'chmod[[:space:]]+777'; then
deny "Blocked: chmod 777 gives everyone read/write/execute. Use more restrictive permissions (e.g., 755 or 644)."
fi
# Block piping curl/wget to shell execution
if echo "$COMMAND" | grep -qE '(curl|wget)[[:space:]].*\|[[:space:]]*(bash|sh|zsh|sudo)'; then
deny "Blocked: piping downloaded content directly to a shell is dangerous. Download first, inspect, then execute."
fi
# Block disk/partition destructive commands
if echo "$COMMAND" | grep -qE '(mkfs|dd[[:space:]]+if=|>[[:space:]]*/dev/)'; then
deny "Blocked: destructive disk operation detected. This can cause irreversible data loss."
fi
# ──────────────────────────────────────────────
# Destructive git operations
# ──────────────────────────────────────────────
# Block git reset --hard (loses uncommitted work permanently)
if echo "$COMMAND" | grep -qE 'git[[:space:]]+reset[[:space:]]+--hard'; then
deny "Blocked: git reset --hard discards uncommitted changes permanently. Use git stash or git reset --soft instead."
fi
# Block git clean -f (permanently deletes untracked files)
if echo "$COMMAND" | grep -qE 'git[[:space:]]+clean[[:space:]]+-[a-zA-Z]*f'; then
deny "Blocked: git clean -f permanently deletes untracked files. Review with git clean -n first, then run manually if intended."
fi
# ──────────────────────────────────────────────
# Accidental package publishing
# ──────────────────────────────────────────────
if echo "$COMMAND" | grep -qE '(npm|yarn|pnpm|bun)[[:space:]]+publish'; then
deny "Blocked: publishing npm packages should be done manually or via CI, not through Claude Code."
fi
if echo "$COMMAND" | grep -qE 'cargo[[:space:]]+publish'; then
deny "Blocked: publishing crates should be done manually or via CI, not through Claude Code."
fi
if echo "$COMMAND" | grep -qE 'gem[[:space:]]+push'; then
deny "Blocked: publishing gems should be done manually or via CI, not through Claude Code."
fi
if echo "$COMMAND" | grep -qE 'twine[[:space:]]+upload'; then
deny "Blocked: publishing Python packages should be done manually or via CI, not through Claude Code."
fi
exit 0