|
#!/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 |