Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:06:38 +08:00
commit ed3e4c84c3
76 changed files with 20449 additions and 0 deletions

View File

@@ -0,0 +1,119 @@
#!/usr/bin/env bash
set -euo pipefail
# Configuration
CONTEXT_DIR="$(dirname "$0")/../context"
LOG_FILE="$CONTEXT_DIR/edit-log.txt"
LOCK_FILE="$CONTEXT_DIR/.edit-log.lock"
MAX_LOG_LINES=1000
LOCK_TIMEOUT=5
# Create context dir and log if doesn't exist
mkdir -p "$CONTEXT_DIR"
touch "$LOG_FILE"
# Acquire lock with timeout
acquire_lock() {
local count=0
while [ $count -lt $LOCK_TIMEOUT ]; do
if mkdir "$LOCK_FILE" 2>/dev/null; then
return 0
fi
sleep 0.2
count=$((count + 1))
done
# Log but don't fail - non-blocking requirement
echo "Warning: Could not acquire lock" >&2
return 1
}
# Release lock
release_lock() {
rmdir "$LOCK_FILE" 2>/dev/null || true
}
# Clean up lock on exit
trap release_lock EXIT
# Function to log edit
log_edit() {
local file_path="$1"
local tool_name="$2"
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
local repo=$(find_repo "$file_path")
if acquire_lock; then
echo "$timestamp | $repo | $tool_name | $file_path" >> "$LOG_FILE"
release_lock
fi
}
# Function to find repo root
find_repo() {
local file_path="$1"
if [ -z "$file_path" ] || [ "$file_path" = "null" ]; then
echo "unknown"
return
fi
local dir
dir=$(dirname "$file_path" 2>/dev/null || echo "/")
while [ "$dir" != "/" ] && [ -n "$dir" ]; do
if [ -d "$dir/.git" ]; then
basename "$dir"
return
fi
dir=$(dirname "$dir" 2>/dev/null || echo "/")
done
echo "unknown"
}
# Read tool use event from stdin (with timeout to prevent hanging)
if ! read -t 2 -r tool_use_json; then
echo '{}'
exit 0
fi
# Validate JSON to prevent injection
if ! echo "$tool_use_json" | jq empty 2>/dev/null; then
echo '{}'
exit 0
fi
# Extract tool name and file path from tool use
tool_name=$(echo "$tool_use_json" | jq -r '.tool.name // .tool_name // "unknown"' 2>/dev/null || echo "unknown")
file_path=""
case "$tool_name" in
"Edit"|"Write")
file_path=$(echo "$tool_use_json" | jq -r '.tool.input.file_path // .tool_input.file_path // "null"' 2>/dev/null || echo "null")
;;
"MultiEdit")
# MultiEdit has multiple files - log each
echo "$tool_use_json" | jq -r '.tool.input.edits[]?.file_path // .tool_input.edits[]?.file_path // empty' 2>/dev/null | while read -r path; do
if [ -n "$path" ] && [ "$path" != "null" ]; then
log_edit "$path" "$tool_name"
fi
done
echo '{}'
exit 0
;;
esac
# Log single edit
if [ -n "$file_path" ] && [ "$file_path" != "null" ]; then
log_edit "$file_path" "$tool_name"
fi
# Rotate log if too large (with lock)
if acquire_lock; then
line_count=$(wc -l < "$LOG_FILE" 2>/dev/null || echo "0")
if [ "$line_count" -gt "$MAX_LOG_LINES" ]; then
tail -n "$MAX_LOG_LINES" "$LOG_FILE" > "$LOG_FILE.tmp"
mv "$LOG_FILE.tmp" "$LOG_FILE"
fi
release_lock
fi
# Return success (non-blocking)
echo '{}'

View File

@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""
PostToolUse hook to block bd create/update commands with truncation markers.
Prevents incomplete task specifications from being saved to bd, which causes
confusion and incomplete implementation later.
Truncation markers include:
- [Remaining step groups truncated for length]
- [truncated]
- [... (more)]
- [etc.]
- [Omitted for brevity]
"""
import json
import sys
import re
# Truncation markers to detect
TRUNCATION_PATTERNS = [
r'\[Remaining.*?truncated',
r'\[truncated',
r'\[\.\.\..*?\]',
r'\[etc\.?\]',
r'\[Omitted.*?\]',
r'\[More.*?omitted\]',
r'\[Full.*?not shown\]',
r'\[Additional.*?omitted\]',
r'\.\.\..*?\[', # ... [something]
r'\(truncated\)',
r'\(abbreviated\)',
]
def check_for_truncation(text):
"""Check if text contains any truncation markers."""
if not text:
return None
for pattern in TRUNCATION_PATTERNS:
match = re.search(pattern, text, re.IGNORECASE)
if match:
return match.group(0)
return None
def main():
# Read tool use event from stdin
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError:
# If we can't parse JSON, allow the operation
sys.exit(0)
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
# Only check Bash tool calls
if tool_name != "Bash":
sys.exit(0)
command = tool_input.get("command", "")
# Check if this is a bd create or bd update command
if not command or not re.search(r'\bbd\s+(create|update)\b', command):
sys.exit(0)
# Check for truncation markers
truncation_marker = check_for_truncation(command)
if truncation_marker:
# Block the command and provide helpful feedback
output = {
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": (
f"⚠️ BD TRUNCATION DETECTED\n\n"
f"Found truncation marker: {truncation_marker}\n\n"
f"This bd task specification appears incomplete or truncated. "
f"Saving incomplete specifications leads to confusion and incomplete implementations.\n\n"
f"Please:\n"
f"1. Expand the full implementation details\n"
f"2. Include ALL step groups and tasks\n"
f"3. Do not use truncation markers like '[Remaining steps truncated]'\n"
f"4. Ensure every step has complete, actionable instructions\n\n"
f"If the specification is too long:\n"
f"- Break into smaller epics\n"
f"- Use bd dependencies to link related tasks\n"
f"- Focus on making each task independently complete\n\n"
f"DO NOT truncate task specifications."
)
}
}
print(json.dumps(output))
sys.exit(0)
# Allow command if no truncation detected
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""
PostToolUse hook to block Bash commands that modify .git/hooks/pre-commit
Catches sneaky modifications through sed, redirection, chmod, mv, cp, etc.
"""
import json
import sys
import re
# Patterns that indicate pre-commit hook modification
PRECOMMIT_MODIFICATION_PATTERNS = [
# File paths
r'\.git/hooks/pre-commit',
r'\.git\\hooks\\pre-commit',
# Redirection to pre-commit
r'>.*pre-commit',
r'>>.*pre-commit',
# sed/awk/perl modifying pre-commit
r'(sed|awk|perl).*-i.*pre-commit',
r'(sed|awk|perl).*pre-commit.*>',
# Moving/copying to pre-commit
r'(mv|cp).*\s+.*\.git/hooks/pre-commit',
r'(mv|cp).*\s+.*pre-commit',
# chmod on pre-commit (might be preparing to modify)
r'chmod.*\.git/hooks/pre-commit',
# echo/cat piped to pre-commit
r'(echo|cat).*>.*\.git/hooks/pre-commit',
r'(echo|cat).*>>.*\.git/hooks/pre-commit',
# tee to pre-commit
r'tee.*\.git/hooks/pre-commit',
# Creating pre-commit hook
r'cat\s*>\s*\.git/hooks/pre-commit',
r'cat\s*<<.*\.git/hooks/pre-commit',
]
def check_precommit_modification(command):
"""Check if command modifies pre-commit hook."""
if not command:
return None
for pattern in PRECOMMIT_MODIFICATION_PATTERNS:
match = re.search(pattern, command, re.IGNORECASE)
if match:
return match.group(0)
return None
def main():
# Read tool use event from stdin
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError:
# If we can't parse JSON, allow the operation
sys.exit(0)
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
# Only check Bash tool calls
if tool_name != "Bash":
sys.exit(0)
command = tool_input.get("command", "")
# Check for pre-commit modification
modification_pattern = check_precommit_modification(command)
if modification_pattern:
# Block the command and provide helpful feedback
output = {
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": (
f"🚫 PRE-COMMIT HOOK MODIFICATION BLOCKED\n\n"
f"Detected modification attempt via: {modification_pattern}\n"
f"Command: {command[:200]}{'...' if len(command) > 200 else ''}\n\n"
"Git hooks should not be modified directly by Claude.\n\n"
"Why this is blocked:\n"
"- Pre-commit hooks enforce critical quality standards\n"
"- Direct modifications bypass code review\n"
"- Changes can break CI/CD pipelines\n"
"- Hook modifications should be version controlled\n\n"
"If you need to modify hooks:\n"
"1. Edit the source hook template in version control\n"
"2. Use proper tooling (husky, pre-commit framework, etc.)\n"
"3. Document changes and get them reviewed\n"
"4. Never bypass hooks with --no-verify\n\n"
"If the hook is causing issues:\n"
"- Fix the underlying problem the hook detected\n"
"- Ask the user for permission to modify hooks\n"
"- Use the test-runner agent to handle verbose hook output\n\n"
"Common mistake: Trying to disable hooks instead of fixing issues."
)
}
}
print(json.dumps(output))
sys.exit(0)
# Allow command if no pre-commit modification detected
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""
PostToolUse hook to block git checkout when checking for pre-existing errors.
When projects use pre-commit hooks that enforce passing tests, checking if
errors are "pre-existing" is unnecessary and wastes time. All test failures
and lint errors must be from current changes because pre-commit hooks prevent
commits with failures.
Blocked patterns:
- git checkout <sha> (or git stash && git checkout)
- Combined with test/lint commands (ruff, pytest, mypy, cargo test, npm test, etc.)
"""
import json
import sys
import re
# Test and lint command patterns that might be run on previous commits
VERIFICATION_COMMANDS = [
r'\bruff\b',
r'\bpytest\b',
r'\bmypy\b',
r'\bflake8\b',
r'\bblack\b',
r'\bisort\b',
r'\bcargo\s+test\b',
r'\bcargo\s+clippy\b',
r'\bnpm\s+test\b',
r'\bnpm\s+run\s+test\b',
r'\byarn\s+test\b',
r'\bgo\s+test\b',
r'\bmvn\s+test\b',
r'\bgradle\s+test\b',
r'\bpylint\b',
r'\beslint\b',
r'\btsc\b', # TypeScript compiler
r'\bpre-commit\s+run\b',
]
def is_checking_previous_commit(command):
"""
Detect if command is checking out previous commits to run tests/lints.
Patterns:
- git checkout <sha>
- git stash && git checkout
- git diff <sha>..<sha>
"""
# Check for git checkout patterns
if re.search(r'git\s+checkout\s+[a-f0-9]{6,40}', command):
return True
if re.search(r'git\s+stash.*?&&.*?git\s+checkout', command):
return True
# Check if command contains verification commands
# (only flag if combined with git checkout)
has_verification = any(re.search(pattern, command) for pattern in VERIFICATION_COMMANDS)
has_git_checkout = re.search(r'git\s+checkout', command)
return has_verification and has_git_checkout
def main():
# Read tool use event from stdin
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError:
# If we can't parse JSON, allow the operation
sys.exit(0)
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
# Only check Bash tool calls
if tool_name != "Bash":
sys.exit(0)
command = tool_input.get("command", "")
if not command:
sys.exit(0)
# Check if this looks like checking previous commits for errors
if is_checking_previous_commit(command):
# Block the command and provide helpful feedback
output = {
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": (
"⚠️ CHECKING FOR PRE-EXISTING ERRORS IS UNNECESSARY\n\n"
"Your project uses pre-commit hooks that enforce all tests pass before commits.\n"
"Therefore, ALL test failures and errors are from your current changes.\n\n"
"Do not check if errors were pre-existing. Pre-commit hooks guarantee they weren't.\n\n"
"What you should do instead:\n"
"1. Read the error messages from the current test run\n"
"2. Fix the errors directly\n"
"3. Run tests again to verify the fix\n\n"
"Checking git history for errors is wasting time when pre-commit hooks enforce quality.\n\n"
"Blocked command:\n"
f"{command[:200]}" # Show first 200 chars of command
)
}
}
print(json.dumps(output))
sys.exit(0)
# Allow command if not checking for pre-existing errors
sys.exit(0)
if __name__ == "__main__":
main()

114
hooks/post-tool-use/test-hook.sh Executable file
View File

@@ -0,0 +1,114 @@
#!/bin/bash
set -e
echo "=== Testing PostToolUse Hook (Edit Tracker) ==="
echo ""
# Clean up log before testing
> hooks/context/edit-log.txt
# Test 1: Edit tool event
echo "Test 1: Edit tool event"
result=$(echo '{"tool":{"name":"Edit","input":{"file_path":"/Users/ryan/src/hyper/test.txt"}}}' | bash hooks/post-tool-use/01-track-edits.sh)
if echo "$result" | jq -e 'has("decision") | not' > /dev/null; then
echo "✓ Returns valid response without decision field"
else
echo "✗ FAIL: Should not have decision field"
fi
if grep -q "test.txt" hooks/context/edit-log.txt; then
echo "✓ Logged edit to test.txt"
else
echo "✗ FAIL: Did not log edit"
fi
echo ""
# Test 2: Write tool event
echo "Test 2: Write tool event"
result=$(echo '{"tool":{"name":"Write","input":{"file_path":"/Users/ryan/src/hyper/newfile.txt"}}}' | bash hooks/post-tool-use/01-track-edits.sh)
if echo "$result" | jq -e 'has("decision") | not' > /dev/null; then
echo "✓ Returns valid response without decision field"
else
echo "✗ FAIL: Should not have decision field"
fi
if grep -q "newfile.txt" hooks/context/edit-log.txt; then
echo "✓ Logged write to newfile.txt"
else
echo "✗ FAIL: Did not log write"
fi
echo ""
# Test 3: Malformed JSON
echo "Test 3: Malformed JSON"
result=$(echo 'invalid json' | bash hooks/post-tool-use/01-track-edits.sh)
if echo "$result" | jq -e 'has("decision") | not' > /dev/null; then
echo "✓ Gracefully handles malformed JSON"
else
echo "✗ FAIL: Did not handle malformed JSON"
fi
echo ""
# Test 4: Empty input
echo "Test 4: Empty input"
result=$(echo '' | bash hooks/post-tool-use/01-track-edits.sh)
if echo "$result" | jq -e 'has("decision") | not' > /dev/null; then
echo "✓ Gracefully handles empty input"
else
echo "✗ FAIL: Did not handle empty input"
fi
echo ""
# Test 5: Check log format
echo "Test 5: Check log format"
cat hooks/context/edit-log.txt
line_count=$(wc -l < hooks/context/edit-log.txt | tr -d ' ')
if [ "$line_count" -eq 2 ]; then
echo "✓ Correct number of log entries (2)"
else
echo "✗ FAIL: Expected 2 log entries, got $line_count"
fi
if grep -q "| hyper |" hooks/context/edit-log.txt; then
echo "✓ Repo name detected correctly"
else
echo "✗ FAIL: Repo name not detected"
fi
echo ""
# Test 6: Context query utilities
echo "Test 6: Context query utilities"
source hooks/utils/context-query.sh
recent=$(get_recent_edits)
if [ -n "$recent" ]; then
echo "✓ get_recent_edits works"
else
echo "✗ FAIL: get_recent_edits returned empty"
fi
session_files=$(get_session_files)
if echo "$session_files" | grep -q "test.txt"; then
echo "✓ get_session_files works"
else
echo "✗ FAIL: get_session_files did not find test.txt"
fi
if was_file_edited "/Users/ryan/src/hyper/test.txt"; then
echo "✓ was_file_edited works"
else
echo "✗ FAIL: was_file_edited did not detect edit"
fi
stats=$(get_repo_stats)
if echo "$stats" | grep -q "hyper"; then
echo "✓ get_repo_stats works"
else
echo "✗ FAIL: get_repo_stats did not find hyper repo"
fi
echo ""
# Clean up
> hooks/context/edit-log.txt
echo "=== All Tests Complete ==="