From 4c8a51785ce7862780a4fb9638ff89566e4376fc Mon Sep 17 00:00:00 2001 From: saurabh Date: Thu, 29 Jan 2026 20:20:10 +0700 Subject: [PATCH] docs: add example pre-exec hooks Includes three ready-to-use safety hooks: - safe-git.sh: Blocks protected branch pushes and force pushes - safe-db.sh: Blocks write operations on remote/production databases - safe-rm.sh: Blocks dangerous file deletions (rm -rf /, etc) --- examples/pre-exec-hooks/README.md | 71 +++++++++++++++++++++++++++++ examples/pre-exec-hooks/safe-db.sh | 55 ++++++++++++++++++++++ examples/pre-exec-hooks/safe-git.sh | 51 +++++++++++++++++++++ examples/pre-exec-hooks/safe-rm.sh | 47 +++++++++++++++++++ 4 files changed, 224 insertions(+) create mode 100644 examples/pre-exec-hooks/README.md create mode 100755 examples/pre-exec-hooks/safe-db.sh create mode 100755 examples/pre-exec-hooks/safe-git.sh create mode 100755 examples/pre-exec-hooks/safe-rm.sh diff --git a/examples/pre-exec-hooks/README.md b/examples/pre-exec-hooks/README.md new file mode 100644 index 000000000..754e5aee7 --- /dev/null +++ b/examples/pre-exec-hooks/README.md @@ -0,0 +1,71 @@ +# Pre-Exec Hooks Examples + +Example shell scripts that intercept Bash/exec tool calls before they run. + +## Installation + +Copy hooks to your workspace: + +```bash +mkdir -p .clawdbot/hooks +cp safe-git.sh safe-db.sh .clawdbot/hooks/ +chmod +x .clawdbot/hooks/*.sh +``` + +## Included Hooks + +### safe-git.sh + +Protects your git workflow: +- ❌ Blocks force pushes (`--force`, `-f`) +- ❌ Blocks pushes to protected branches (main, develop, staging, production) +- ❌ Blocks remote modifications +- ✅ Allows normal git operations + +### safe-db.sh + +Protects production databases: +- ❌ Blocks INSERT, UPDATE, DELETE, DROP on remote hosts +- ❌ Blocks migrations/seeds targeting staging/production +- ✅ Allows SELECT queries on remote DBs +- ✅ Allows all operations on localhost + +### safe-rm.sh + +Prevents catastrophic deletions: +- ❌ Blocks `rm -rf /` +- ❌ Blocks `rm` on system directories +- ❌ Blocks `rm -rf *` (wildcard) +- ✅ Allows normal file deletion + +## Testing + +```bash +# Test safe-git.sh +echo '{"tool_name":"exec","tool_input":{"command":"git push origin main"}}' | ./safe-git.sh +# → {"decision": "deny", "reason": "🚫 Pushing to protected branches is blocked..."} + +# Test safe-db.sh +echo '{"tool_name":"exec","tool_input":{"command":"psql -h prod.db.com -c \"DROP TABLE users\""}}' | ./safe-db.sh +# → {"decision": "deny", "reason": "🚫 Non-SELECT operations on remote databases are blocked."} +``` + +## Writing Your Own + +See [Pre-Exec Hooks Documentation](../../docs/tools/pre-exec-hooks.md) for the full protocol. + +Basic template: + +```bash +#!/bin/bash +INPUT=$(cat) +COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') + +# Your logic here +if should_block "$COMMAND"; then + echo '{"decision": "deny", "reason": "Your message here"}' + exit 0 +fi + +echo '{"decision": "approve"}' +``` diff --git a/examples/pre-exec-hooks/safe-db.sh b/examples/pre-exec-hooks/safe-db.sh new file mode 100755 index 000000000..6f67ae2a0 --- /dev/null +++ b/examples/pre-exec-hooks/safe-db.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Clawdbot PreToolUse Hook: STRICT read-only for non-local databases +# Based on Claude Code hooks in ~/code/yieldnest/yieldnest-api/.claude/hooks/ +# +# Only SELECT allowed on remote DBs - everything else blocked + +INPUT=$(cat) +TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty') +COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') + +# Only process Bash/exec tools +[[ "$TOOL" != "Bash" && "$TOOL" != "exec" ]] && echo '{"decision": "approve"}' && exit 0 + +# Known non-local DB patterns (customize as needed) +REMOTE_HOSTS="gondola|maglev|railway|\.up\.railway\.app|staging|prod|amazonaws\.com|azure|supabase|neon\.tech|planetscale" + +# Check if command involves any remote database +if echo "$COMMAND" | grep -qiE "($REMOTE_HOSTS)"; then + + # Block any non-SELECT SQL operations + if echo "$COMMAND" | grep -qiE "(INSERT|UPDATE|DELETE|DROP|TRUNCATE|ALTER|CREATE|GRANT|REVOKE|EXECUTE|COPY|VACUUM|ANALYZE|REINDEX|REFRESH|LOCK|COMMENT)\s"; then + echo '{"decision": "deny", "reason": "🚫 Write operation blocked on remote DB. Only SELECT queries allowed on staging/prod. Use local DB for writes."}' + exit 0 + fi + + # Block psql -c with non-SELECT + if echo "$COMMAND" | grep -qiE "psql.*-c\s*['\"]?\s*(INSERT|UPDATE|DELETE|DROP|TRUNCATE|ALTER|CREATE)"; then + echo '{"decision": "deny", "reason": "🚫 Write operation blocked on remote DB. Only SELECT allowed."}' + exit 0 + fi + + # Block piped writes + if echo "$COMMAND" | grep -qiE "(echo|cat|printf).*\|.*psql" && echo "$COMMAND" | grep -qiE "(INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)"; then + echo '{"decision": "deny", "reason": "🚫 Piped write operation blocked on remote DB."}' + exit 0 + fi +fi + +# Block ORM/migration commands on non-local +if echo "$COMMAND" | grep -qiE "(prisma|typeorm|knex|sequelize|drizzle|migrate|seed|db:push|db:seed|sync)"; then + if echo "$COMMAND" | grep -qiE "($REMOTE_HOSTS|APP_ENV.*(prod|staging)|NODE_ENV.*(prod|staging))"; then + echo '{"decision": "deny", "reason": "🚫 Migrations/seeds blocked on remote DB. Only run on local."}' + exit 0 + fi +fi + +# Block connection strings pointing to remote with potential writes +if echo "$COMMAND" | grep -qiE "(DATABASE_URL|postgres://|postgresql://|mysql://|mongodb).*($REMOTE_HOSTS)"; then + if echo "$COMMAND" | grep -qiE "(INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|migrate|seed|push|sync)"; then + echo '{"decision": "deny", "reason": "🚫 Non-read operation blocked on remote DB."}' + exit 0 + fi +fi + +echo '{"decision": "approve"}' diff --git a/examples/pre-exec-hooks/safe-git.sh b/examples/pre-exec-hooks/safe-git.sh new file mode 100755 index 000000000..ae8df7dcd --- /dev/null +++ b/examples/pre-exec-hooks/safe-git.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Clawdbot PreToolUse Hook: Prevent pushes to protected branches +# Based on Claude Code hooks in ~/code/yieldnest/yieldnest-api/.claude/hooks/ +# +# Allows: all git operations EXCEPT pushing to protected branches +# Protected branches: develop, production, staging, main + +INPUT=$(cat) +TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty') +COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') + +# Only process Bash/exec tools +[[ "$TOOL" != "Bash" && "$TOOL" != "exec" ]] && echo '{"decision": "approve"}' && exit 0 + +# Block git remote modifications +if echo "$COMMAND" | grep -qE 'git\s+remote\s+(add|remove|rm|set-url|rename)'; then + echo '{"decision": "deny", "reason": "🚫 Modifying git remotes is blocked."}' + exit 0 +fi + +# Block force push operations +# Match -f or --force as standalone arguments (preceded by whitespace, not as part of branch name) +if echo "$COMMAND" | grep -qE 'git\s+push\s+(-f\s|--force\s|-f$|--force$)'; then + echo '{"decision": "deny", "reason": "🚫 Force push is blocked."}' + exit 0 +fi +if echo "$COMMAND" | grep -qE 'git\s+push\s+\S+\s+(-f\s|--force\s|-f$|--force$)'; then + echo '{"decision": "deny", "reason": "🚫 Force push is blocked."}' + exit 0 +fi +if echo "$COMMAND" | grep -qE 'git\s+push\s+\S+\s+\S+\s+(-f|--force)(\s|$)'; then + echo '{"decision": "deny", "reason": "🚫 Force push is blocked."}' + exit 0 +fi + +# Block pushes to protected branches +if echo "$COMMAND" | grep -qE '(^|\s|;|\||&&)git\s+push'; then + # Check for protected branch names in the push command + if echo "$COMMAND" | grep -qE 'git\s+push\s+[^\s]+\s+(develop|production|staging|main)(\s|$|:)'; then + echo '{"decision": "deny", "reason": "🚫 Pushing to protected branches (develop/production/staging/main) is blocked."}' + exit 0 + fi + + # Check for push to origin with protected branch + if echo "$COMMAND" | grep -qE 'git\s+push\s+origin\s+(develop|production|staging|main)'; then + echo '{"decision": "deny", "reason": "🚫 Pushing to protected branches (develop/production/staging/main) is blocked."}' + exit 0 + fi +fi + +echo '{"decision": "approve"}' diff --git a/examples/pre-exec-hooks/safe-rm.sh b/examples/pre-exec-hooks/safe-rm.sh new file mode 100755 index 000000000..28db82c3d --- /dev/null +++ b/examples/pre-exec-hooks/safe-rm.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# Clawdbot PreToolUse Hook: Prevent dangerous rm operations +# +# Blocks: +# - rm -rf / +# - rm on home directory +# - rm on common system directories +# - rm without -i on important directories + +INPUT=$(cat) +TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty') +COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') + +# Only process Bash/exec tools +[[ "$TOOL" != "Bash" && "$TOOL" != "exec" ]] && echo '{"decision": "approve"}' && exit 0 + +# Skip if not an rm command +if ! echo "$COMMAND" | grep -qE '(^|\s|;|\||&&)rm\s'; then + echo '{"decision": "approve"}' + exit 0 +fi + +# Block rm -rf / +if echo "$COMMAND" | grep -qE 'rm\s+.*-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*\s+/?(\s|$|;|\||&&)'; then + echo '{"decision": "deny", "reason": "🚫 rm -rf / is blocked. Use trash instead for safe deletion."}' + exit 0 +fi + +# Block rm on home directory +if echo "$COMMAND" | grep -qE 'rm\s+.*(\$HOME|~|/home/[^/]+)\s*/?(\s|$|;|\||&&)'; then + echo '{"decision": "deny", "reason": "🚫 rm on home directory is blocked. Use trash instead."}' + exit 0 +fi + +# Block rm on system directories +if echo "$COMMAND" | grep -qE 'rm\s+.*(^|\s)/(usr|bin|sbin|etc|var|opt|lib|System|Applications)\s*/?'; then + echo '{"decision": "deny", "reason": "🚫 rm on system directories is blocked."}' + exit 0 +fi + +# Block rm -rf without explicit path (could be dangerous) +if echo "$COMMAND" | grep -qE 'rm\s+.*-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*\s*\*'; then + echo '{"decision": "deny", "reason": "🚫 rm -rf * is too dangerous. Be more specific or use trash."}' + exit 0 +fi + +echo '{"decision": "approve"}'