Skip to content

Instantly share code, notes, and snippets.

@walis85300
Last active March 19, 2026 14:42
Show Gist options
  • Select an option

  • Save walis85300/f81e55a2f4855ef71ad89258c1eb350e to your computer and use it in GitHub Desktop.

Select an option

Save walis85300/f81e55a2f4855ef71ad89258c1eb350e to your computer and use it in GitHub Desktop.
Claude Code PreToolUse hook: table-driven dangerous command guard with ask/deny decisions
#!/bin/bash
# Dangerous Command Guard for Claude Code
#
# Table-driven: each rule is "decision :: regex :: message"
# "deny" = hard block, "ask" = user confirms
# Separator is " :: " (space-colon-colon-space) to avoid conflicts with regex pipes.
#
# Add new rules to RULES below — no code changes needed.
set -euo pipefail
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || echo "")
[ -z "$COMMAND" ] && exit 0
decide() {
jq -n --arg d "$1" --arg r "$2" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: $d,
permissionDecisionReason: $r
}
}'
exit 0
}
# RULES: decision :: regex :: message
# Separator is " :: ". Regex can contain | for alternation.
# Lines starting with # are ignored.
read -r -d '' RULES << 'EOF' || true
# DENY: production only
deny :: git\s+push\s+origin\s+(main|master|production)\b :: Push directo a main/production
deny :: kubectl\s+(delete|patch|exec|rollout).*-n\s*(prod|production)\b :: Operación destructiva en production
deny :: kubectl\s+-n\s*(prod|production)\b.*(delete|patch|exec|rollout) :: Operación destructiva en production
# ASK: git
ask :: git\s+push\s+.*(--force|-f\b) :: git push --force
ask :: git\s+reset\s+--hard :: git reset --hard
ask :: git\s+clean\s+-[a-z]*f :: git clean (borra untracked)
ask :: git\s+(checkout|restore)\s+--?\s*\. :: Descarta todos los cambios locales
ask :: git\s+branch\s+-D\s :: git branch -D (force delete)
ask :: git\s+push\s+origin\s+staging\b :: Push directo a staging
# ASK: kubectl
ask :: kubectl\s+delete\s+(pod|deployment|svc|statefulset) :: kubectl delete workloads
ask :: kubectl\s+patch\s+(deployment|statefulset) :: kubectl patch infra
ask :: kubectl\s+rollout\s+restart.*-n\s*(core|staging|messaging) :: rollout restart namespace compartido
ask :: kubectl\s+scale.*--replicas\s*=?\s*0 :: kubectl scale a 0 replicas
# ASK: kubectl exec
ask :: kubectl\s+exec.*(->delete\(|DELETE\s+FROM|TRUNCATE) :: SQL DELETE/TRUNCATE via kubectl
ask :: kubectl\s+exec.*(DB::(insert|update|statement)|INSERT\s+INTO|UPDATE\s+.*SET) :: SQL mutación via kubectl
ask :: kubectl\s+exec.*config:(clear|cache) :: config cache en pod
ask :: kubectl\s+exec.*queue:(flush|clear) :: Borrando jobs de cola
ask :: kubectl\s+exec.*migrate\s+--force :: Migración --force
ask :: kubectl\s+exec.*rm\s+-rf :: rm -rf en pod
ask :: kubectl\s+exec.*(FLUSHALL|FLUSHDB) :: Redis FLUSH en pod
# ASK: SQL, filesystem, docker, secrets, APIs
ask :: DROP\s+(TABLE|DATABASE) :: DROP TABLE/DATABASE
ask :: \brm\s+-rf\s :: rm -rf
ask :: docker\s+system\s+prune :: docker system prune
ask :: docker(-compose|\s+compose)\s+down\s+-v :: docker down -v (borra volúmenes)
ask :: doppler\s+secrets\s+(set|delete).*--config\s*prod :: Doppler secrets en prod
ask :: curl\s+.*-X\s*DELETE :: curl DELETE request
EOF
# Engine: split on " :: ", preserving regex pipes
while IFS= read -r line; do
[ -z "$line" ] && continue
[[ "$line" == \#* ]] && continue
decision="${line%% :: *}"
rest="${line#* :: }"
pattern="${rest%% :: *}"
message="${rest#* :: }"
if printf '%s' "$COMMAND" | grep -qiE "$pattern" 2>/dev/null; then
decide "$decision" "$message"
fi
done <<< "$RULES"
exit 0

Claude Code Dangerous Command Guard

A PreToolUse hook that intercepts destructive Bash commands before execution. Works in yolo/bypass mode — adds a safety net without slowing down safe operations.

How it works

You (or Claude) runs a command
        ↓
    Hook intercepts
        ↓
  ┌─────────────────┐
  │ Match rules table │
  └────────┬────────┘
           ↓
  ┌────────────────┐
  │ deny → blocked │  (production-only, no override)
  │ ask  → prompt  │  (user confirms, then proceeds)
  │ none → allow   │  (passes silently)
  └────────────────┘

Three decisions:

  • deny: Hard block. Push to main, kubectl in production.
  • ask: Shows confirmation prompt. User approves or rejects.
  • allow: Everything else passes without friction.

Setup

1. Create the hook file

mkdir -p ~/.claude/hooks
curl -o ~/.claude/hooks/dangerous-command-guard.sh \
  https://gist.githubusercontent.com/walis85300/f81e55a2f4855ef71ad89258c1eb350e/raw/dangerous-command-guard.sh
chmod +x ~/.claude/hooks/dangerous-command-guard.sh

2. Add to settings.json

Add this to ~/.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/dangerous-command-guard.sh"
          }
        ]
      }
    ]
  }
}

3. Requires jq

# macOS
brew install jq

# Ubuntu/Debian
sudo apt-get install jq

Adding new rules

Open ~/.claude/hooks/dangerous-command-guard.sh and add a line to the RULES table:

ask :: your\s+regex\s+pattern :: Description shown to user
  • Separator is :: (space-colon-colon-space)
  • Regex is case-insensitive (grep -iE)
  • Pipes | work in regex for alternation
  • Use deny instead of ask for hard blocks

Example — block terraform destroy:

ask :: terraform\s+destroy :: Terraform destroy — removes infrastructure

What's covered out of the box

Decision Pattern
deny git push origin main/production
deny kubectl destructive ops in production namespace
ask git push --force, git reset --hard, git clean -f, git branch -D
ask kubectl delete/patch/scale-to-0, rollout restart in shared namespaces
ask SQL mutations via kubectl exec (DELETE, TRUNCATE, INSERT, UPDATE)
ask rm -rf, docker system prune, docker-compose down -v
ask DROP TABLE/DATABASE
ask Doppler secrets changes in prod config
ask curl -X DELETE
ask Redis FLUSHALL/FLUSHDB via kubectl exec
ask migrate --force, queue:flush, config:cache in running pods

How it uses the Claude Code API

The hook outputs JSON to stdout using the official permissionDecision API:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "ask",
    "permissionDecisionReason": "Description of why this is dangerous"
  }
}
  • "ask" → Claude Code shows a confirmation prompt to the user
  • "deny" → Claude Code blocks the command entirely
  • No output + exit 0 → command proceeds normally

Self-evolving rules

The hook is designed to grow over time. After each session where a potentially dangerous command was executed without a guard rule, add it to the table. The format is simple enough that even Claude can suggest additions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment