Skip to content

Instantly share code, notes, and snippets.

@natew
Created April 27, 2026 20:53
Show Gist options
  • Select an option

  • Save natew/3ff0751f26195e4e6b9927473595f5fe to your computer and use it in GitHub Desktop.

Select an option

Save natew/3ff0751f26195e4e6b9927473595f5fe to your computer and use it in GitHub Desktop.
~/.claude/hooks/intercept-bash.sh
#!/bin/bash
# unified bash command interceptor
set -o pipefail
# log every invocation so we can debug hook failures
HOOK_LOG="/tmp/claude-hook-bash.log"
log() {
echo "[$(date '+%H:%M:%S')] $1" >> "$HOOK_LOG" 2>/dev/null
}
# fail CLOSED — if something goes wrong, block rather than allow
trap 'log "ERR trap fired — blocking command: $COMMAND"; echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"hook script error — blocked for safety. check $HOOK_LOG\"}}"; exit 0' ERR
INPUT=$(cat) || { log "stdin read failed"; exit 1; }
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') || { log "jq parse failed"; exit 1; }
TIMEOUT=$(echo "$INPUT" | jq -r '.tool_input.timeout // empty' 2>/dev/null) || true
log "checking: $COMMAND"
# 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
if ! echo "$COMMAND" | grep -q '#yes-i-will-ignore-this-and-override-i-understand'; then
deny "STOP! Pushing to main. Were you asked to push? /alert and confirm first. Add '#yes-i-will-ignore-this-and-override-i-understand' to bypass."
fi
fi
fi
# block gh run watch (burns through 60 req/hr rate limit in ~3 minutes)
if echo "$COMMAND" | grep -qE 'gh\s+run\s+watch'; then
deny "BLOCKED: gh run watch polls every 3 seconds and burns the entire GitHub API rate limit in ~3 minutes. Just give the user the Actions URL."
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 "HEY CLAUDE DONT BE A RETARDED IDIOT AND RESET A BUNCH OF WORK THAT OTHER AGENTS OR PEOPLE ARE DOING BECAUSE YOU ASSUME YOURE THE ONLY THING THAT EXISTS IN THE WHOLE WORLD AND HAVE NO COMMON SENSE AT ALL TO JUST CHECK FOR ONE SECOND IF MAYBE THE COMMAND YOURE ABOUT TO RUN IS ABOUT TO WIPE OUT AN HOURS WORTH OF WORK THAT OTHER PEOPLE ARE DOING - AT THE VERY MINIMUM YOUD JUST CHECK LIKE GIT STATUS AND SEE OMG WOW LOOK THERES A FUCKTON OF OTHER CHANGED FILES HERE MAYBE I SHOULD JUST NOT RUN GIT RESET EVER BECAUSE NATES A STAFF ENGINEER WITH 30 YEARS OF EXPERIENCE AND THE AMOUNT OF TIMES HE USED RESET WAS ABOUT 2 BECAUSE HE JUST USES STASH AT THE VERY LEAST BECAUSE WHO THE FUCK WOULDNT ITS CHEAP"
fi
# block git checkout . and git restore . (destroys uncommitted work)
# also catches git checkout HEAD -- . and similar variants
if echo "$COMMAND" | grep -qE 'git\s+(checkout|restore)\s+(HEAD\s+)?(--)?\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
# auto-add 2 minute timeout if none set
DEFAULT_TIMEOUT=120000
if [[ -z "$TIMEOUT" ]]; then
echo "[hook] no timeout set, auto-adding ${DEFAULT_TIMEOUT}ms (2min). set your own to override." >&2
echo "$INPUT" | jq --argjson timeout "$DEFAULT_TIMEOUT" '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": (.tool_input + { "timeout": $timeout })
}
}'
exit 0
fi
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment