Skip to content

Instantly share code, notes, and snippets.

@fongandrew
Created January 11, 2026 16:48
Show Gist options
  • Select an option

  • Save fongandrew/32222869f25202c8532f705f2fb9abd1 to your computer and use it in GitHub Desktop.

Select an option

Save fongandrew/32222869f25202c8532f705f2fb9abd1 to your computer and use it in GitHub Desktop.
Review the guidelines stop hook for Claude Code
#!/usr/bin/env bash
# Review modified files against relevant guides on Stop
#
# This hook fires when Claude finishes work. It checks if any files matching
# configured patterns were modified and, if so, blocks the stop and asks
# Claude to review them against the appropriate guide.
#
# To add new patterns, add entries to the PATTERNS and GUIDES arrays below.
set -e
# =============================================================================
# CONFIGURATION: Map file patterns to guide documents
# =============================================================================
# Add patterns and their corresponding guides at matching indices.
# The regex patterns are matched against the full file path using grep -E.
PATTERNS=(
'\.test\.(ts|tsx)$'
'\.spec\.(ts|tsx)$'
'\.css$'
)
GUIDES=(
"docs/unit-testing-guide.md"
"docs/playwright-testing-guide.md"
"docs/css-guide.md"
)
# =============================================================================
# HOOK LOGIC (no need to modify below unless changing behavior)
# =============================================================================
# Read hook input from stdin
INPUT=$(cat)
# Prevent infinite loops: if a stop hook already triggered, allow stop
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
echo '{"decision": "approve"}'
exit 0
fi
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty')
if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
echo '{"decision": "approve"}'
exit 0
fi
# Find the line number of the last Stop hook event (if any)
# We only want to review files modified AFTER the last stop hook ran
# Stop hook events are recorded as type:"system" subtype:"stop_hook_summary"
LAST_STOP_LINE=$(grep -n -E '"subtype"[[:space:]]*:[[:space:]]*"stop_hook_summary"' "$TRANSCRIPT_PATH" 2>/dev/null | tail -1 | cut -d: -f1 || echo "0")
LAST_STOP_LINE=${LAST_STOP_LINE:-0}
# Extract file paths from Edit/Write tool calls, but only after the last Stop hook
# The transcript is JSONL format - each line is a separate JSON object
# Tool uses are nested inside message.content[] for assistant messages
ALL_MODIFIED_FILES=$(tail -n +$((LAST_STOP_LINE + 1)) "$TRANSCRIPT_PATH" | while read -r line; do
echo "$line" | jq -r '
.message.content[]?
| select(.type == "tool_use")
| select(.name == "Edit" or .name == "Write")
| .input.file_path // empty
' 2>/dev/null
done | grep -v '^$' | sort -u || true)
if [ -z "$ALL_MODIFIED_FILES" ]; then
echo '{"decision": "approve"}'
exit 0
fi
# Build review instructions for each pattern that has matching files
REVIEW_SECTIONS=""
for i in "${!PATTERNS[@]}"; do
pattern="${PATTERNS[$i]}"
guide="${GUIDES[$i]}"
# Find files matching this pattern
MATCHING_FILES=$(echo "$ALL_MODIFIED_FILES" | grep -E "$pattern" || true)
if [ -n "$MATCHING_FILES" ]; then
FILE_LIST=$(echo "$MATCHING_FILES" | sed 's/^/ - /')
REVIEW_SECTIONS="${REVIEW_SECTIONS}
## Files matching pattern \`${pattern}\`:
${FILE_LIST}
Review against: ${guide}
"
fi
done
# If any patterns matched, block stop and request review
if [ -n "$REVIEW_SECTIONS" ]; then
# Properly escape the reason for JSON
REASON="Files were modified that require review against project guidelines.${REVIEW_SECTIONS}
Please review the listed files against their respective guides and fix any violations before stopping."
# Use jq to properly escape the string for JSON
JSON_REASON=$(echo "$REASON" | jq -Rs .)
echo "{\"decision\": \"block\", \"reason\": ${JSON_REASON}}"
else
echo '{"decision": "approve"}'
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment