From 0d2f046ec6c687b587d1ea17170c891b189c625b Mon Sep 17 00:00:00 2001 From: Tym Rabchuk Date: Sun, 5 Apr 2026 18:30:59 -0400 Subject: [PATCH] Delete 5 legacy hook scripts superseded by lang-review.sh and stop-gate.sh Removed: dirty-bit.sh, go-nil-return.sh, go-review.sh, go-vet-stop.sh, shell-compat.sh. All were consolidated in PR #16 but the files were kept. None are registered in hooks.json. Updated README architecture diagram to reference current hook names. --- README.md | 2 +- hooks/dirty-bit.sh | 95 -------------------------------------- hooks/go-nil-return.sh | 101 ----------------------------------------- hooks/go-review.sh | 72 ----------------------------- hooks/go-vet-stop.sh | 82 --------------------------------- hooks/shell-compat.sh | 82 --------------------------------- 6 files changed, 1 insertion(+), 433 deletions(-) delete mode 100755 hooks/dirty-bit.sh delete mode 100755 hooks/go-nil-return.sh delete mode 100755 hooks/go-review.sh delete mode 100755 hooks/go-vet-stop.sh delete mode 100755 hooks/shell-compat.sh diff --git a/README.md b/README.md index 4aa5e78..6ca4c68 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Verify with `/context-mode:ctx-doctor` (plugin install) or check MCP tools are a │ research, scrape (no slash command needed) │ ├──────────────────────────────────────────────────────┤ │ Always active: devkit hooks (safety, security, │ -│ audit, slop, go-review, dirty-bit, go-vet, compat) │ +│ audit, slop, lang-review, security, stop-gate) │ ├──────────────────────────────────────────────────────┤ │ Meta: hookify (create hooks), skill-creator (skills) │ │ context-mode (token management) │ diff --git a/hooks/dirty-bit.sh b/hooks/dirty-bit.sh deleted file mode 100755 index 47f51a2..0000000 --- a/hooks/dirty-bit.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/bin/bash -# devkit Stop hook — dirty-bit feedback loop -# -# Tracks which domains were modified and warns if the appropriate -# verification (tests/lint) hasn't been run for each touched domain. -# -# Domains: backend (Go/Python), frontend (TS/JS/JSX/TSX), config (YAML/JSON/TOML), -# test files, SQL/migrations -# -# Stop hook schema: -# { "decision": "approve" | "block", "reason": "string" } - -set -euo pipefail - -INPUT=$(cat) -TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript // empty') - -# Get modified files from git -REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) -CHANGED_FILES=$(cd "$REPO_ROOT" && git diff --name-only HEAD 2>/dev/null; git diff --name-only --cached 2>/dev/null; git diff --name-only 2>/dev/null) - -if [ -z "$CHANGED_FILES" ]; then - jq -n '{ decision: "approve" }' - exit 0 -fi - -# Classify changed files into domains -BACKEND=false -FRONTEND=false -CONFIG=false -SQL=false -DOMAINS_TOUCHED="" - -while IFS= read -r file; do - case "$file" in - *.go|*.py|*.rb|*.java|*.rs) - BACKEND=true ;; - *.ts|*.tsx|*.js|*.jsx|*.vue|*.svelte) - FRONTEND=true ;; - *.yml|*.yaml|*.json|*.toml|*.ini|*.env*) - CONFIG=true ;; - *.sql|**/migrations/*|**/migrate/*) - SQL=true ;; - esac -done <<< "$CHANGED_FILES" - -# Build list of touched domains -if [ "$BACKEND" = "true" ]; then DOMAINS_TOUCHED="$DOMAINS_TOUCHED backend"; fi -if [ "$FRONTEND" = "true" ]; then DOMAINS_TOUCHED="$DOMAINS_TOUCHED frontend"; fi -if [ "$CONFIG" = "true" ]; then DOMAINS_TOUCHED="$DOMAINS_TOUCHED config"; fi -if [ "$SQL" = "true" ]; then DOMAINS_TOUCHED="$DOMAINS_TOUCHED sql"; fi - -# If only one domain or no code domains, approve -DOMAIN_COUNT=$(echo "$DOMAINS_TOUCHED" | wc -w | tr -d ' ') -if [ "$DOMAIN_COUNT" -le 1 ]; then - jq -n '{ decision: "approve" }' - exit 0 -fi - -# Multiple domains touched — check for test evidence per domain -MISSING_VERIFICATION="" - -if [ "$BACKEND" = "true" ]; then - if ! echo "$TRANSCRIPT" | grep -qiE '(go test|pytest|python.*test|cargo test|bundle exec.*test|ALL_PASSING|ALL_TESTS_PASSING)'; then - MISSING_VERIFICATION="$MISSING_VERIFICATION backend" - fi -fi - -if [ "$FRONTEND" = "true" ]; then - if ! echo "$TRANSCRIPT" | grep -qiE '(npm test|npx jest|npx vitest|yarn test|pnpm test|ALL_PASSING|ALL_TESTS_PASSING)'; then - MISSING_VERIFICATION="$MISSING_VERIFICATION frontend" - fi -fi - -if [ "$SQL" = "true" ]; then - if ! echo "$TRANSCRIPT" | grep -qiE '(migrate|migration.*up|schema.*applied|ALL_PASSING)'; then - MISSING_VERIFICATION="$MISSING_VERIFICATION sql/migrations" - fi -fi - -# If everything verified, approve -if [ -z "$MISSING_VERIFICATION" ]; then - jq -n '{ decision: "approve" }' - exit 0 -fi - -# Multiple domains touched, some unverified — block -DOMAINS_MSG=$(echo "$DOMAINS_TOUCHED" | xargs) -MISSING_MSG=$(echo "$MISSING_VERIFICATION" | xargs) - -jq -n --arg domains "$DOMAINS_MSG" --arg missing "$MISSING_MSG" '{ - decision: "block", - reason: ("Cross-domain changes detected (touched: " + $domains + "). Missing test/verification evidence for: " + $missing + ". Run the relevant test suite for each domain before completing.") -}' -exit 0 diff --git a/hooks/go-nil-return.sh b/hooks/go-nil-return.sh deleted file mode 100755 index f46a197..0000000 --- a/hooks/go-nil-return.sh +++ /dev/null @@ -1,101 +0,0 @@ -#!/bin/bash -# devkit PostToolUse hook — detects Go functions that always return nil error -# -# Scans written Go code for functions with error return types where -# every return statement returns nil for the error. This pattern -# silently swallows failures and is a top LLM-generated bug category. -# -# PostToolUse hook schema: -# { "hookSpecificOutput": { "hookEventName": "PostToolUse", "additionalContext": "string" } } - -set -euo pipefail - -INPUT=$(cat) -TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty') -FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') -CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // empty') - -# Only check Go files on Edit/Write -case "$FILE_PATH" in - *.go) ;; - *) exit 0 ;; -esac -if [ "$TOOL_NAME" != "Edit" ] && [ "$TOOL_NAME" != "Write" ]; then - exit 0 -fi -[ -z "$CONTENT" ] && exit 0 - -# Use awk to find functions that return error but only ever return nil -# Strategy: track function signatures with error returns, then check -# if ALL return statements in that function use nil for the error position -WARNINGS=$(echo "$CONTENT" | awk ' - # Match function declarations that return error (last return type) - /^func .*\)\s*(\(.*error\)|error)\s*\{/ { - fname = $0 - sub(/\{.*/, "", fname) - in_func = 1 - brace_depth = 0 - has_return = 0 - has_non_nil_err = 0 - # Count all braces on the declaration line itself - line = $0 - for (j = 1; j <= length(line); j++) { - c = substr(line, j, 1) - if (c == "{") brace_depth++ - if (c == "}") brace_depth-- - } - next - } - - in_func { - # Track brace depth - line = $0 - for (i = 1; i <= length(line); i++) { - c = substr(line, i, 1) - if (c == "{") brace_depth++ - if (c == "}") brace_depth-- - } - - # Check return statements - if ($0 ~ /return /) { - has_return = 1 - # Check if error position is non-nil (not "nil" or "nil)") - if ($0 !~ /,\s*nil\s*$/ && $0 !~ /return nil\s*$/ && $0 !~ /,\s*nil\s*\)/) { - has_non_nil_err = 1 - } - } - - # End of function - if (brace_depth <= 0) { - if (has_return && !has_non_nil_err) { - # Strip leading whitespace from function name - gsub(/^[[:space:]]+/, "", fname) - print fname - } - in_func = 0 - } - } -') - -if [ -n "$WARNINGS" ]; then - # Limit to first 3 functions to avoid noise - FUNCS=$(echo "$WARNINGS" | head -3) - COUNT=$(echo "$WARNINGS" | wc -l | tr -d ' ') - MSG="Go nil-error pattern: ${COUNT} function(s) return error but only ever return nil. This silently swallows failures:" - while IFS= read -r fn; do - MSG="$MSG\n - $fn" - done <<< "$FUNCS" - if [ "$COUNT" -gt 3 ]; then - MSG="$MSG\n ... and $((COUNT - 3)) more" - fi - - jq -n --arg msg "$MSG" '{ - hookSpecificOutput: { - hookEventName: "PostToolUse", - additionalContext: $msg - } - }' - exit 0 -fi - -exit 0 diff --git a/hooks/go-review.sh b/hooks/go-review.sh deleted file mode 100755 index bf0884e..0000000 --- a/hooks/go-review.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/bin/bash -# devkit PostToolUse hook — Go code quality patterns -# -# Checks written Go code for common LLM-generated bug patterns: -# 1. Accessing result fields in error paths -# 2. Goroutines reading shared maps without protection -# 3. Functions that always return nil error -# 4. Unsanitized user input in filepath operations -# -# PostToolUse hook schema: -# { "hookSpecificOutput": { "hookEventName": "PostToolUse", "additionalContext": "string" } } - -set -euo pipefail - -INPUT=$(cat) -TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty') -FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') -CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // empty') - -# Only check Go files -case "$FILE_PATH" in - *.go) ;; - *) exit 0 ;; -esac - -# Only check Edit/Write -if [ "$TOOL_NAME" != "Edit" ] && [ "$TOOL_NAME" != "Write" ]; then - exit 0 -fi - -if [ -z "$CONTENT" ]; then - exit 0 -fi - -WARNINGS="" - -# Pattern 1: Accessing result after error check -# Detects: if err != nil { ... result. or res. ... } on nearby lines -# Uses awk instead of grep -P to stay macOS-compatible -if echo "$CONTENT" | grep -qE 'if err != nil'; then - if echo "$CONTENT" | awk '/if err != nil \{/{found=1; buf=""} found{buf=buf $0 "\n"; if(/\}/){if(buf ~ /result\.|res\./){exit 0} found=0}} END{exit 1}'; then - WARNINGS="$WARNINGS\n- Possible result field access inside error path (result may be zero-value when err != nil)" - fi -fi - -# Pattern 2: Goroutines with shared map access -if echo "$CONTENT" | grep -qE 'go func' && echo "$CONTENT" | grep -qE 'map\[string\]'; then - if ! echo "$CONTENT" | grep -qE '(sync\.Mutex|sync\.RWMutex|sync\.Map|snapshot|Snap)'; then - WARNINGS="$WARNINGS\n- Goroutines detected with map usage but no visible mutex/snapshot — verify concurrent map access is safe" - fi -fi - -# Pattern 3: filepath.Join with unsanitized variable -if echo "$CONTENT" | grep -qE 'filepath\.Join.*\b(name|input|arg|param|user)'; then - if ! echo "$CONTENT" | grep -qE '(regexp|Regexp|MustCompile|MatchString|ValidateName|sanitize)'; then - WARNINGS="$WARNINGS\n- filepath.Join with potentially unsanitized input — validate before constructing paths" - fi -fi - -if [ -n "$WARNINGS" ]; then - MSG=$(printf "Go code quality check:%b" "$WARNINGS") - jq -n --arg msg "$MSG" '{ - hookSpecificOutput: { - hookEventName: "PostToolUse", - additionalContext: $msg - } - }' - exit 0 -fi - -# All clear -exit 0 diff --git a/hooks/go-vet-stop.sh b/hooks/go-vet-stop.sh deleted file mode 100755 index 189a442..0000000 --- a/hooks/go-vet-stop.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/bin/bash -# devkit Stop hook — enforces go vet + race detector on Go changes -# -# When Go files were modified in the session, runs go vet and -# go test -race to catch concurrency bugs before session completes. -# -# Stop hook schema: -# { "decision": "approve" | "block", "reason": "string" } - -set -euo pipefail - -# Check if any Go files were modified -REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) -GO_CHANGES=$(cd "$REPO_ROOT" && { - git diff --name-only HEAD 2>/dev/null - git diff --name-only --cached 2>/dev/null - git diff --name-only 2>/dev/null -} | grep '\.go$' | sort -u) - -if [ -z "$GO_CHANGES" ]; then - jq -n '{ decision: "approve" }' - exit 0 -fi - -# Find the Go module root (directory containing go.mod) -GO_MOD_DIR="" -for candidate in "$REPO_ROOT" "$REPO_ROOT/src" "$REPO_ROOT/cmd"; do - if [ -f "$candidate/go.mod" ]; then - GO_MOD_DIR="$candidate" - break - fi -done - -if [ -z "$GO_MOD_DIR" ]; then - # No go.mod found — can't run vet, approve and move on - jq -n '{ decision: "approve" }' - exit 0 -fi - -# Run go vet -VET_OUTPUT=$(cd "$GO_MOD_DIR" && go vet ./... 2>&1) || true -if [ -n "$VET_OUTPUT" ]; then - jq -n --arg msg "go vet found issues in modified Go files. Fix before completing:\n$VET_OUTPUT" '{ - decision: "block", - reason: $msg - }' - exit 0 -fi - -# Run go test -race on packages with changes (limited to 60s) -# Extract unique package directories from changed files -PACKAGES="" -while IFS= read -r file; do - dir=$(dirname "$file") - # Convert filesystem path to Go package path relative to module - rel=$(echo "$dir" | sed "s|^${GO_MOD_DIR#$REPO_ROOT/}/||; s|^${GO_MOD_DIR#$REPO_ROOT/}$|.|") - if [ "$rel" = "$dir" ]; then - rel="./$(echo "$dir" | sed "s|^src/||")" - fi - PACKAGES="$PACKAGES ./$rel" -done <<< "$GO_CHANGES" -PACKAGES=$(echo "$PACKAGES" | tr ' ' '\n' | sort -u | tr '\n' ' ') - -if [ -n "$PACKAGES" ]; then - # Use perl alarm for POSIX-compatible timeout (macOS has no `timeout` command) - RACE_OUTPUT=$(cd "$GO_MOD_DIR" && perl -e 'alarm 60; exec @ARGV' -- go test -race -count=1 $PACKAGES 2>&1) || RACE_EXIT=$? - if [ "${RACE_EXIT:-0}" -ne 0 ]; then - # Check if it's specifically a race condition - if echo "$RACE_OUTPUT" | grep -qE 'DATA RACE|race detected'; then - RACE_LINES=$(echo "$RACE_OUTPUT" | grep -A5 'DATA RACE' | head -20) - jq -n --arg msg "Race condition detected in modified Go packages:\n$RACE_LINES" '{ - decision: "block", - reason: $msg - }' - exit 0 - fi - # Test failure but not a race — don't block on this hook (dirty-bit handles test coverage) - fi -fi - -jq -n '{ decision: "approve" }' -exit 0 diff --git a/hooks/shell-compat.sh b/hooks/shell-compat.sh deleted file mode 100755 index efebcb0..0000000 --- a/hooks/shell-compat.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/bin/bash -# devkit PreToolUse hook — shell script portability check -# -# Flags non-portable constructs in shell scripts that break on macOS: -# - grep -P (Perl regex, BSD grep doesn't support it) -# - sed -i without '' (GNU vs BSD sed) -# - readlink -f (use realpath or manual resolution) -# - stat --format (GNU stat, not BSD) -# - xargs -d (GNU xargs, not BSD) -# - date -d (GNU date, not BSD) -# -# PreToolUse hook schema: -# { "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "ask", ... } } - -set -euo pipefail - -INPUT=$(cat) -TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty') -FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') -CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // empty') - -# Only check shell scripts -case "$FILE_PATH" in - *.sh) ;; - *) exit 0 ;; -esac - -# Only check Edit/Write -if [ "$TOOL_NAME" != "Edit" ] && [ "$TOOL_NAME" != "Write" ]; then - exit 0 -fi - -[ -z "$CONTENT" ] && exit 0 - -# Session dedup — use PPID (stable across hook invocations within one Claude session) -SEEN_FILE="/tmp/devkit-shellcompat-seen-${PPID:-0}" - -check_compat() { - local pattern="$1" - local message="$2" - local key="${FILE_PATH}:${pattern}" - - if echo "$CONTENT" | grep -qE "$pattern"; then - if [ -f "$SEEN_FILE" ] && grep -qF "$key" "$SEEN_FILE" 2>/dev/null; then - return - fi - echo "$key" >> "$SEEN_FILE" 2>/dev/null - - jq -n --arg reason "$message" '{ - hookSpecificOutput: { - hookEventName: "PreToolUse", - permissionDecision: "ask", - permissionDecisionReason: $reason - } - }' - exit 0 - fi -} - -check_compat 'grep\s+(-[a-zA-Z]*P|--perl-regexp)' \ - "Portability: grep -P (Perl regex) is unavailable on macOS — use grep -E, awk, or perl instead" - -check_compat 'sed\s+-i\s+[^'"'"'"]' \ - "Portability: sed -i without '' breaks on macOS BSD sed — use sed -i '' for in-place edits" - -check_compat 'readlink\s+-f\b' \ - "Portability: readlink -f is GNU-only — use realpath or manual loop on macOS" - -check_compat 'stat\s+--format' \ - "Portability: stat --format is GNU-only — use stat -f on macOS" - -check_compat 'xargs\s+-d\b' \ - "Portability: xargs -d is GNU-only — use tr + xargs or while-read on macOS" - -check_compat 'date\s+-d\b' \ - "Portability: date -d is GNU-only — use date -j -f on macOS" - -check_compat 'mktemp\s+--suffix' \ - "Portability: mktemp --suffix is GNU-only — use mktemp with template on macOS" - -# All clear -exit 0