Skip to content

Instantly share code, notes, and snippets.

@natew
Last active February 16, 2026 19:12
Show Gist options
  • Select an option

  • Save natew/1ce159ad11509bdb6688acbcbd9eeb33 to your computer and use it in GitHub Desktop.

Select an option

Save natew/1ce159ad11509bdb6688acbcbd9eeb33 to your computer and use it in GitHub Desktop.
Claude Code hooks - block destructive git commands
#!/bin/bash
# unified bash command interceptor
set -o pipefail
# silently exit on any error - don't block claude
trap 'exit 0' ERR
INPUT=$(cat 2>/dev/null) || exit 0
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null) || exit 0
# no command = nothing to check
[[ -z "$COMMAND" ]] && exit 0
deny() {
jq -n --arg reason "$1" '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": $reason
}
}'
exit 0
}
warn() {
jq -n --arg reason "$1" '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
"permissionDecisionReason": $reason
}
}'
exit 0
}
# block pkill -f (can kill unrelated processes)
if echo "$COMMAND" | grep -qE 'pkill\s+(-\w+\s+)*-f|pkill\s+-f'; then
deny "pkill -f is blocked - can kill unrelated processes. Use: kill-port <port>"
fi
# block release commands that skip tests
if echo "$COMMAND" | grep -qE 'bun release|npm publish'; then
if ! echo "$COMMAND" | grep -q '\-\-nate-told-me-i-could'; then
if echo "$COMMAND" | grep -qiE 'SKIP.*TEST|--skip-test|--no-test'; then
deny "NEVER skip tests during release. Fix them. (bypass: --nate-told-me-i-could)"
fi
fi
fi
# block git push to main/master without explicit ask
if echo "$COMMAND" | grep -qE 'git\s+push'; then
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || true
if [[ "$CURRENT_BRANCH" == "main" || "$CURRENT_BRANCH" == "master" ]]; then
deny "STOP! Pushing to main. Were you asked to push? /alert and confirm first."
fi
fi
# block git reset --hard (destroys uncommitted work)
if echo "$COMMAND" | grep -qE 'git\s+reset\s+.*--hard|git\s+reset\s+--hard'; then
deny "BLOCKED: git reset --hard destroys uncommitted work. Use --soft or stash instead."
fi
# block git checkout . and git restore . (destroys uncommitted work)
if echo "$COMMAND" | grep -qE 'git\s+(checkout|restore)\s+\.'; then
deny "BLOCKED: This destroys uncommitted work. Be specific about files or stash first."
fi
# block git clean -f (deletes untracked files)
if echo "$COMMAND" | grep -qE 'git\s+clean\s+.*-f'; then
deny "BLOCKED: git clean -f deletes untracked files permanently."
fi
# warn on git stash (might stash other agent's work)
# add "# safe" to command to bypass after checking
if echo "$COMMAND" | grep -qE 'git\s+stash(\s|$)'; then
if ! echo "$COMMAND" | grep -q '# safe'; then
deny "STOP: Before stashing - run git status. If there's work that isn't yours, /alert and ask. Add '# safe' to bypass."
fi
fi
# warn on git reset --soft (safer but still changes state)
# add "# safe" to command to bypass after checking
if echo "$COMMAND" | grep -qE 'git\s+reset\s+.*--soft|git\s+reset\s+--soft'; then
if ! echo "$COMMAND" | grep -q '# safe'; then
deny "STOP: Before reset --soft - run git status/log. Make sure this is intentional. Add '# safe' to bypass."
fi
fi
exit 0
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/intercept-bash.sh"
}
]
}
]
},
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment