Skip to content

Instantly share code, notes, and snippets.

@ceaksan
Created April 9, 2026 06:40
Show Gist options
  • Select an option

  • Save ceaksan/578118fce37616e67efcd0bb37feec50 to your computer and use it in GitHub Desktop.

Select an option

Save ceaksan/578118fce37616e67efcd0bb37feec50 to your computer and use it in GitHub Desktop.
Decision Gate Checklist + Destructive Command Guard for Claude Code

Decision Gate Checklist

Evaluate every new dependency, pattern, API, or architectural decision against these 8 criteria.

Criteria

Criterion Question Red Flag
Benefit What concrete problem does it solve? Measurable impact? "Might need it later", "because it's best practice"
Necessity What happens if we don't? Is there a simpler alternative? Adding something new when existing solution is sufficient
Burden Maintenance cost, learning curve, files/layers affected? 5+ file changes for 1 feature, new abstraction layer
Conflict Does it conflict with existing patterns/dependencies/conventions? Two libraries doing the same thing
Performance Bundle size, runtime cost, render cycle, network impact? Barrel import, unnecessary re-render, N+1 query
Security Does it open new attack surface? Input validation? Auth affected? Unsanitized user input, new endpoint
Bottleneck Does it create SPOF, rate limit, resource contention? All load on single worker, unbounded queue
Currency Is dependency maintained? API deprecated? Breaking change incoming? Last release 1+ year ago, approaching EOL

Application Rules

  • Not required for trivial changes (typo, 1-2 line fix)
  • When adding new dependency: check npm trends, bundle size, last release date
  • When updating existing dependency: check changelog and breaking changes
  • One sentence per criterion is sufficient; write "OK" if no issues
  • If red flag exists, state it clearly and suggest alternative

Template

### Decision Gate: [Technology/Pattern Name]

| Criterion | Assessment |
|-----------|------------|
| Benefit | |
| Necessity | |
| Burden | |
| Conflict | |
| Performance | |
| Security | |
| Bottleneck | |
| Currency | |

**Decision:** ADOPT / REJECT / DEFER
**Reasoning:** One sentence.

Example: Evaluating a New Library

### Decision Gate: Zustand (State Management)

| Criterion | Assessment |
|-----------|------------|
| Benefit | Replaces 200+ lines of custom context providers. OK |
| Necessity | Current solution works but doesn't scale. PROCEED WITH CAUTION |
| Burden | 1 file change, minimal API surface. OK |
| Conflict | No overlap with existing state management. OK |
| Performance | 1.1KB gzipped, no runtime overhead. OK |
| Security | No new attack surface. OK |
| Bottleneck | No SPOF risk. OK |
| Currency | Active maintenance, 40K+ stars, monthly releases. OK |

**Decision:** ADOPT
**Reasoning:** Replaces complex custom solution with well-maintained, minimal library.
#!/bin/bash
# Guard: Block destructive bash commands before execution
# PreToolUse hook for Claude Code
# exit 2 = BLOCK (command will not execute)
# exit 0 + stderr = WARN (command executes with warning)
# exit 0 = PASS
#
# Setup: Add as PreToolUse hook in ~/.claude/settings.json
# {
# "hooks": {
# "PreToolUse": [
# { "matcher": "Bash", "command": "bash ~/.claude/hooks/guard-destructive.sh" }
# ]
# }
# }
DEBT_FILE="$HOME/.claude/coffee-debt"
LOG_FILE="$HOME/.claude/coffee-log.jsonl"
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')
if [ -z "$command" ]; then
exit 0
fi
# --- SAFE PATTERNS (check first, short-circuit) ---
# Safe rm targets: build artifacts, caches, temp dirs
SAFE_RM_TARGETS="node_modules|dist|build|\.next|__pycache__|\.cache|coverage|\.turbo|\.venv|\.pytest_cache|\.ruff_cache|\.mypy_cache|\.tox|\.eggs|\.egg-info|tmp|temp|\.parcel-cache|\.nuxt|\.output|\.astro"
is_safe_rm() {
local cmd="$1"
local targets=""
local skip_rm=1
for word in $cmd; do
if [ $skip_rm -eq 1 ]; then
skip_rm=0
continue
fi
case "$word" in
-*) continue ;;
*) targets="$targets $word" ;;
esac
done
[ -z "$targets" ] && return 1
for target in $targets; do
local base=$(basename "$target")
if ! echo "$base" | grep -qE "^(${SAFE_RM_TARGETS})$"; then
return 1
fi
done
return 0
}
# --- Check a single command segment ---
check_single_command() {
local cmd="$1"
# === BLOCK PATTERNS ===
# Recursive deletion with dangerous paths
if echo "$cmd" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+|--recursive\s+)'; then
if ! is_safe_rm "$cmd"; then
if echo "$cmd" | grep -qE 'rm\s.*\s*(~/|/Users|/home|\$HOME|/\s|/\s*$|\.\.)'; then
echo "BLOCK|rm_dangerous_path|file_deletion|BLOCKED: Recursive deletion targeting dangerous path."
return
fi
echo "BLOCK|rm_recursive|file_deletion|BLOCKED: Recursive file deletion detected. Safe targets (node_modules, dist, build, etc.) are auto-allowed."
return
fi
fi
# Git reset --hard
if echo "$cmd" | grep -qE 'git\s+reset\s+--hard'; then
echo "BLOCK|git_reset_hard|git_destructive|BLOCKED: git reset --hard discards all uncommitted changes permanently."
return
fi
# Git push --force
if echo "$cmd" | grep -qE 'git\s+push\s+.*(-f|--force-with-lease|--force)'; then
echo "BLOCK|git_push_force|git_destructive|BLOCKED: Force push rewrites remote history."
return
fi
# Git clean -f
if echo "$cmd" | grep -qE 'git\s+clean\s+.*-[a-zA-Z]*f'; then
echo "BLOCK|git_clean|git_destructive|BLOCKED: git clean -f permanently deletes untracked files."
return
fi
# Git branch -D (force delete)
if echo "$cmd" | grep -qE 'git\s+branch\s+.*-D'; then
echo "BLOCK|git_branch_force_delete|git_destructive|BLOCKED: git branch -D deletes branch even with unmerged changes."
return
fi
# Git checkout/restore discard all
if echo "$cmd" | grep -qE 'git\s+checkout\s+--\s*\.|git\s+restore\s+\.'; then
echo "BLOCK|git_discard_all|git_destructive|BLOCKED: This discards ALL working directory changes."
return
fi
# Database DROP/TRUNCATE
if echo "$cmd" | grep -qiE 'DROP\s+(TABLE|DATABASE|SCHEMA)|TRUNCATE\s+TABLE'; then
echo "BLOCK|db_destructive|database_destructive|BLOCKED: DROP/TRUNCATE permanently destroys database objects."
return
fi
# Cloud resource deletion (customize for your providers)
if echo "$cmd" | grep -qE '(vercel|railway|fly|neon)\s+(rm|remove|delete|destroy)'; then
echo "BLOCK|cloud_delete|cloud_destructive|BLOCKED: Cloud resource deletion detected."
return
fi
# Docker force prune
if echo "$cmd" | grep -qE 'docker\s+system\s+prune\s+.*-a'; then
echo "BLOCK|docker_prune|docker_destructive|BLOCKED: docker system prune -a removes all unused images."
return
fi
# chmod 777
if echo "$cmd" | grep -qE 'chmod\s+(-R\s+)?777'; then
echo "BLOCK|chmod_777|security|BLOCKED: chmod 777 makes files world-accessible."
return
fi
# === WARN PATTERNS ===
# DELETE without WHERE
if echo "$cmd" | grep -qiE 'DELETE\s+FROM\s+\w+\s*;?\s*$'; then
echo "WARN|delete_no_where|database_risky|WARNING: DELETE without WHERE clause deletes ALL rows."
return
fi
# DB/backup file deletion
if echo "$cmd" | grep -qE 'rm\s+.*\.(db|sqlite|sqlite3|sql|backup|bak|dump)'; then
echo "WARN|rm_db_file|data_risky|WARNING: Deleting database/backup file."
return
fi
# Env file deletion
if echo "$cmd" | grep -qE 'rm\s+.*\.env'; then
echo "WARN|rm_env_file|credential_risky|WARNING: Deleting .env file with potential credentials."
return
fi
# git stash drop
if echo "$cmd" | grep -qE 'git\s+stash\s+drop'; then
echo "WARN|git_stash_drop|git_risky|WARNING: Dropped stashes cannot be recovered."
return
fi
}
# --- Main execution ---
# Check for chained commands (&&, ||, ;)
check_all() {
local full_cmd="$1"
# Check full command first
local result=$(check_single_command "$full_cmd")
if [ -n "$result" ]; then
echo "$result"
return
fi
# Split on chain operators and check segments
echo "$full_cmd" | tr '&' '\n' | tr '|' '\n' | tr ';' '\n' | while IFS= read -r segment; do
segment=$(echo "$segment" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
[ -z "$segment" ] && continue
result=$(check_single_command "$segment")
if [ -n "$result" ]; then
echo "$result"
return
fi
done
}
result=$(check_all "$command")
# No match = safe
if [ -z "$result" ]; then
exit 0
fi
# Parse result
severity=$(echo "$result" | cut -d'|' -f1)
reason=$(echo "$result" | cut -d'|' -f2)
category=$(echo "$result" | cut -d'|' -f3)
message=$(echo "$result" | cut -d'|' -f4-)
echo -e "$message" >&2
# Log to coffee-log.jsonl (optional, remove if not using Coffee Debt)
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
CMD_SNIPPET=$(echo "$command" | head -c 300 | jq -Rs '.' 2>/dev/null || echo '""')
LOG_TYPE="blocked"
[ "$severity" = "WARN" ] && LOG_TYPE="warned"
if [ "$severity" = "BLOCK" ]; then
if [ -f "$DEBT_FILE" ]; then
TOTAL=$(cat "$DEBT_FILE" 2>/dev/null || echo "0")
TOTAL=$((TOTAL + 1))
echo "$TOTAL" > "$DEBT_FILE"
fi
exit 2
fi
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment