|
| 1 | +#!/usr/bin/env bash |
| 2 | +# PreToolUse hook (.claude/settings.json, matcher: Bash): denies the |
| 3 | +# two command families this repo's rules mark never-granted, after |
| 4 | +# each cost a real incident (docs/goals/0191's evidence bar -- no hook |
| 5 | +# for a problem that hasn't occurred): |
| 6 | +# - `pkill -f` / `killall`: a broad pkill once took down the |
| 7 | +# production mill-server LaunchAgent; kills must target own PIDs |
| 8 | +# or lsof-resolved ports. |
| 9 | +# - `git push --force[-with-lease]` / `-f` and history-rewrite |
| 10 | +# plumbing (filter-branch/filter-repo): never granted (CLAUDE.md). |
| 11 | +# `commit --amend` and plain `git rebase` stay JUDGEMENT, not |
| 12 | +# hooks: both are legitimate when explicitly asked (the rule's own |
| 13 | +# wording), and the pr-shepherd's instructed rebases are a |
| 14 | +# standing legal flow -- a hook cannot see "was asked". |
| 15 | +# Patterns are anchored to a command-segment start (after ^ ; & |) so |
| 16 | +# a commit message or echoed string MENTIONING these words is never a |
| 17 | +# false positive -- false negatives are acceptable here, a wrongly |
| 18 | +# blocked session is not. Fails OPEN (exit 0) on any internal error, |
| 19 | +# same contract as hook-build-guard.sh; exit 2 is the documented |
| 20 | +# unconditional PreToolUse deny. |
| 21 | +set -uo pipefail |
| 22 | + |
| 23 | +input="$(cat)" |
| 24 | + |
| 25 | +if ! command -v jq >/dev/null 2>&1; then |
| 26 | + exit 0 |
| 27 | +fi |
| 28 | +if ! echo "$input" | jq -e . >/dev/null 2>&1; then |
| 29 | + exit 0 |
| 30 | +fi |
| 31 | + |
| 32 | +tool_name="$(echo "$input" | jq -r '.tool_name // empty')" |
| 33 | +if [ "$tool_name" != "Bash" ]; then |
| 34 | + exit 0 |
| 35 | +fi |
| 36 | + |
| 37 | +command_str="$(echo "$input" | jq -r '.tool_input.command // empty')" |
| 38 | +if [ -z "$command_str" ]; then |
| 39 | + exit 0 |
| 40 | +fi |
| 41 | + |
| 42 | +seg='(^|[;&|][[:space:]]*|&&[[:space:]]*|\|\|[[:space:]]*)(sudo[[:space:]]+)?' |
| 43 | + |
| 44 | +if echo "$command_str" | grep -qE "${seg}pkill([[:space:]]+[^;&|[:space:]]+)*[[:space:]]+-[a-zA-Z]*f"; then |
| 45 | + echo "Blocked: pkill -f is never allowed here (a broad pkill once killed the production mill-server). Kill only your own PIDs, or resolve the port with lsof -ti and kill that PID." >&2 |
| 46 | + exit 2 |
| 47 | +fi |
| 48 | + |
| 49 | +if echo "$command_str" | grep -qE "${seg}killall([[:space:]]|$)"; then |
| 50 | + echo "Blocked: killall is never allowed here (same incident class as pkill -f). Kill only your own PIDs, or resolve the port with lsof -ti and kill that PID." >&2 |
| 51 | + exit 2 |
| 52 | +fi |
| 53 | + |
| 54 | +if echo "$command_str" | grep -qE "${seg}git[[:space:]]+push([[:space:]]+[^;&|[:space:]]+)*[[:space:]]+(--force(-with-lease)?|-f)([[:space:]]|$)"; then |
| 55 | + echo "Blocked: force-push is never granted in this repo (CLAUDE.md). Fix forward with a new commit instead." >&2 |
| 56 | + exit 2 |
| 57 | +fi |
| 58 | + |
| 59 | +if echo "$command_str" | grep -qE "${seg}git[[:space:]]+(filter-branch|filter-repo)([[:space:]]|$)"; then |
| 60 | + echo "Blocked: history rewrites are never granted in this repo (CLAUDE.md)." >&2 |
| 61 | + exit 2 |
| 62 | +fi |
| 63 | + |
| 64 | +exit 0 |
0 commit comments