mirror of https://github.com/garrytan/gstack.git
112 lines
4.4 KiB
Bash
Executable File
112 lines
4.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# check-careful.sh — PreToolUse hook for /careful skill
|
|
# Reads JSON from stdin, checks Bash command for destructive patterns.
|
|
# Returns {"permissionDecision":"ask","message":"..."} to warn, or {} to allow.
|
|
set -euo pipefail
|
|
|
|
# Read stdin (JSON with tool_input)
|
|
INPUT=$(cat)
|
|
|
|
# Parse JSON before inspecting the command. Regex extraction cannot distinguish
|
|
# escaped quotes from the end of a JSON string and can hide a destructive tail.
|
|
# A malformed hook payload is itself unsafe, so fail closed and ask.
|
|
if ! CMD=$(printf '%s' "$INPUT" | python3 -c '
|
|
import json, sys
|
|
data = json.load(sys.stdin)
|
|
if not isinstance(data, dict):
|
|
raise ValueError("hook payload must be an object")
|
|
tool_input = data.get("tool_input", {})
|
|
if not isinstance(tool_input, dict):
|
|
raise ValueError("tool_input must be an object")
|
|
command = tool_input.get("command", "")
|
|
if not isinstance(command, str):
|
|
raise ValueError("command must be a string")
|
|
sys.stdout.write(command)
|
|
' 2>/dev/null); then
|
|
echo '{"permissionDecision":"ask","message":"[careful] Could not parse the command payload safely. Review it before proceeding."}'
|
|
exit 0
|
|
fi
|
|
|
|
# If we still couldn't extract a command, allow
|
|
if [ -z "$CMD" ]; then
|
|
echo '{}'
|
|
exit 0
|
|
fi
|
|
|
|
# Normalize: lowercase for case-insensitive SQL matching
|
|
CMD_LOWER=$(printf '%s' "$CMD" | tr '[:upper:]' '[:lower:]')
|
|
|
|
# --- Check for safe exceptions (one standalone rm of build artifacts) ---
|
|
# Match the complete command. Parsing only the last rm is unsafe because shell
|
|
# syntax or comments can hide an earlier destructive command, for example:
|
|
# rm -rf / # rm -rf node_modules
|
|
# Unknown syntax fails closed and falls through to the destructive checks.
|
|
if [[ "$CMD" != *$'\n'* ]] && printf '%s' "$CMD" | grep -qE '^[[:space:]]*rm[[:space:]]+(-[a-zA-Z]*r[a-zA-Z]*[[:space:]]+|--recursive[[:space:]]+)(([^[:space:];&|#]*/)?(node_modules|\.next|dist|__pycache__|\.cache|build|\.turbo|coverage)[[:space:]]*)+$' 2>/dev/null; then
|
|
echo '{}'
|
|
exit 0
|
|
fi
|
|
|
|
# --- Destructive pattern checks ---
|
|
WARN=""
|
|
PATTERN=""
|
|
|
|
# rm -rf / rm -r / rm --recursive
|
|
if printf '%s' "$CMD" | grep -qE 'rm\s+(-[a-zA-Z]*r|--recursive)' 2>/dev/null; then
|
|
WARN="Destructive: recursive delete (rm -r). This permanently removes files."
|
|
PATTERN="rm_recursive"
|
|
fi
|
|
|
|
# DROP TABLE / DROP DATABASE
|
|
if [ -z "$WARN" ] && printf '%s' "$CMD_LOWER" | grep -qE 'drop\s+(table|database)' 2>/dev/null; then
|
|
WARN="Destructive: SQL DROP detected. This permanently deletes database objects."
|
|
PATTERN="drop_table"
|
|
fi
|
|
|
|
# TRUNCATE
|
|
if [ -z "$WARN" ] && printf '%s' "$CMD_LOWER" | grep -qE '\btruncate\b' 2>/dev/null; then
|
|
WARN="Destructive: SQL TRUNCATE detected. This deletes all rows from a table."
|
|
PATTERN="truncate"
|
|
fi
|
|
|
|
# git push --force / git push -f
|
|
if [ -z "$WARN" ] && printf '%s' "$CMD" | grep -qE 'git\s+push\s+.*(-f\b|--force)' 2>/dev/null; then
|
|
WARN="Destructive: git force-push rewrites remote history. Other contributors may lose work."
|
|
PATTERN="git_force_push"
|
|
fi
|
|
|
|
# git reset --hard
|
|
if [ -z "$WARN" ] && printf '%s' "$CMD" | grep -qE 'git\s+reset\s+--hard' 2>/dev/null; then
|
|
WARN="Destructive: git reset --hard discards all uncommitted changes."
|
|
PATTERN="git_reset_hard"
|
|
fi
|
|
|
|
# git checkout . / git restore .
|
|
if [ -z "$WARN" ] && printf '%s' "$CMD" | grep -qE 'git\s+(checkout|restore)\s+\.' 2>/dev/null; then
|
|
WARN="Destructive: discards all uncommitted changes in the working tree."
|
|
PATTERN="git_discard"
|
|
fi
|
|
|
|
# kubectl delete
|
|
if [ -z "$WARN" ] && printf '%s' "$CMD" | grep -qE 'kubectl\s+delete' 2>/dev/null; then
|
|
WARN="Destructive: kubectl delete removes Kubernetes resources. May impact production."
|
|
PATTERN="kubectl_delete"
|
|
fi
|
|
|
|
# docker rm -f / docker system prune
|
|
if [ -z "$WARN" ] && printf '%s' "$CMD" | grep -qE 'docker\s+(rm\s+-f|system\s+prune)' 2>/dev/null; then
|
|
WARN="Destructive: Docker force-remove or prune. May delete running containers or cached images."
|
|
PATTERN="docker_destructive"
|
|
fi
|
|
|
|
# --- Output ---
|
|
if [ -n "$WARN" ]; then
|
|
# Log hook fire event (pattern name only, never command content)
|
|
mkdir -p ~/.gstack/analytics 2>/dev/null || true
|
|
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
|
|
|
|
WARN_ESCAPED=$(printf '%s' "$WARN" | sed 's/"/\\"/g')
|
|
printf '{"permissionDecision":"ask","message":"[careful] %s"}\n' "$WARN_ESCAPED"
|
|
else
|
|
echo '{}'
|
|
fi
|