|
| 1 | +#!/usr/bin/env bash |
| 2 | +# check-careful.sh — PreToolUse hook for /careful skill |
| 3 | +# Reads JSON from stdin, checks Bash command for destructive patterns. |
| 4 | +# Returns {"permissionDecision":"ask","message":"..."} to warn, or {} to allow. |
| 5 | +set -euo pipefail |
| 6 | + |
| 7 | +# Read stdin (JSON with tool_input) |
| 8 | +INPUT=$(cat) |
| 9 | + |
| 10 | +# Extract the "command" field value from tool_input |
| 11 | +# Try grep/sed first (handles 99% of cases), fall back to Python for escaped quotes |
| 12 | +CMD=$(printf '%s' "$INPUT" | grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:[[:space:]]*"//;s/"$//' || true) |
| 13 | + |
| 14 | +# Python fallback if grep returned empty (e.g., escaped quotes in command) |
| 15 | +if [ -z "$CMD" ]; then |
| 16 | + CMD=$(printf '%s' "$INPUT" | python3 -c 'import sys,json; print(json.loads(sys.stdin.read()).get("tool_input",{}).get("command",""))' 2>/dev/null || true) |
| 17 | +fi |
| 18 | + |
| 19 | +# If we still couldn't extract a command, allow |
| 20 | +if [ -z "$CMD" ]; then |
| 21 | + echo '{}' |
| 22 | + exit 0 |
| 23 | +fi |
| 24 | + |
| 25 | +# Normalize: lowercase for case-insensitive SQL matching |
| 26 | +CMD_LOWER=$(printf '%s' "$CMD" | tr '[:upper:]' '[:lower:]') |
| 27 | + |
| 28 | +# --- Check for safe exceptions (rm -rf of build artifacts) --- |
| 29 | +if printf '%s' "$CMD" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+|--recursive\s+)' 2>/dev/null; then |
| 30 | + SAFE_ONLY=true |
| 31 | + RM_ARGS=$(printf '%s' "$CMD" | sed -E 's/.*rm\s+(-[a-zA-Z]+\s+)*//;s/--recursive\s*//') |
| 32 | + for target in $RM_ARGS; do |
| 33 | + case "$target" in |
| 34 | + */node_modules|node_modules|*/\.next|\.next|*/dist|dist|*/__pycache__|__pycache__|*/\.cache|\.cache|*/build|build|*/\.turbo|\.turbo|*/coverage|coverage) |
| 35 | + ;; # safe target |
| 36 | + -*) |
| 37 | + ;; # flag, skip |
| 38 | + *) |
| 39 | + SAFE_ONLY=false |
| 40 | + break |
| 41 | + ;; |
| 42 | + esac |
| 43 | + done |
| 44 | + if [ "$SAFE_ONLY" = true ]; then |
| 45 | + echo '{}' |
| 46 | + exit 0 |
| 47 | + fi |
| 48 | +fi |
| 49 | + |
| 50 | +# --- Destructive pattern checks --- |
| 51 | +WARN="" |
| 52 | +PATTERN="" |
| 53 | + |
| 54 | +# rm -rf / rm -r / rm --recursive |
| 55 | +if printf '%s' "$CMD" | grep -qE 'rm\s+(-[a-zA-Z]*r|--recursive)' 2>/dev/null; then |
| 56 | + WARN="Destructive: recursive delete (rm -r). This permanently removes files." |
| 57 | + PATTERN="rm_recursive" |
| 58 | +fi |
| 59 | + |
| 60 | +# DROP TABLE / DROP DATABASE |
| 61 | +if [ -z "$WARN" ] && printf '%s' "$CMD_LOWER" | grep -qE 'drop\s+(table|database)' 2>/dev/null; then |
| 62 | + WARN="Destructive: SQL DROP detected. This permanently deletes database objects." |
| 63 | + PATTERN="drop_table" |
| 64 | +fi |
| 65 | + |
| 66 | +# TRUNCATE |
| 67 | +if [ -z "$WARN" ] && printf '%s' "$CMD_LOWER" | grep -qE '\btruncate\b' 2>/dev/null; then |
| 68 | + WARN="Destructive: SQL TRUNCATE detected. This deletes all rows from a table." |
| 69 | + PATTERN="truncate" |
| 70 | +fi |
| 71 | + |
| 72 | +# git push --force / git push -f |
| 73 | +if [ -z "$WARN" ] && printf '%s' "$CMD" | grep -qE 'git\s+push\s+.*(-f\b|--force)' 2>/dev/null; then |
| 74 | + WARN="Destructive: git force-push rewrites remote history. Other contributors may lose work." |
| 75 | + PATTERN="git_force_push" |
| 76 | +fi |
| 77 | + |
| 78 | +# git reset --hard |
| 79 | +if [ -z "$WARN" ] && printf '%s' "$CMD" | grep -qE 'git\s+reset\s+--hard' 2>/dev/null; then |
| 80 | + WARN="Destructive: git reset --hard discards all uncommitted changes." |
| 81 | + PATTERN="git_reset_hard" |
| 82 | +fi |
| 83 | + |
| 84 | +# git checkout . / git restore . |
| 85 | +if [ -z "$WARN" ] && printf '%s' "$CMD" | grep -qE 'git\s+(checkout|restore)\s+\.' 2>/dev/null; then |
| 86 | + WARN="Destructive: discards all uncommitted changes in the working tree." |
| 87 | + PATTERN="git_discard" |
| 88 | +fi |
| 89 | + |
| 90 | +# kubectl delete |
| 91 | +if [ -z "$WARN" ] && printf '%s' "$CMD" | grep -qE 'kubectl\s+delete' 2>/dev/null; then |
| 92 | + WARN="Destructive: kubectl delete removes Kubernetes resources. May impact production." |
| 93 | + PATTERN="kubectl_delete" |
| 94 | +fi |
| 95 | + |
| 96 | +# docker rm -f / docker system prune |
| 97 | +if [ -z "$WARN" ] && printf '%s' "$CMD" | grep -qE 'docker\s+(rm\s+-f|system\s+prune)' 2>/dev/null; then |
| 98 | + WARN="Destructive: Docker force-remove or prune. May delete running containers or cached images." |
| 99 | + PATTERN="docker_destructive" |
| 100 | +fi |
| 101 | + |
| 102 | +# --- Output --- |
| 103 | +if [ -n "$WARN" ]; then |
| 104 | + # Log hook fire event (pattern name only, never command content) |
| 105 | + mkdir -p ~/.gstack/analytics 2>/dev/null || true |
| 106 | + echo '{"event":"hook_fire","skill":"careful","pattern":"'"$PATTERN"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "unknown")'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true |
| 107 | + |
| 108 | + WARN_ESCAPED=$(printf '%s' "$WARN" | sed 's/"/\\"/g') |
| 109 | + printf '{"permissionDecision":"ask","message":"[careful] %s"}\n' "$WARN_ESCAPED" |
| 110 | +else |
| 111 | + echo '{}' |
| 112 | +fi |
0 commit comments