feat: add 4 ClaudeKit-inspired hooks (checkpoint, validation, file-guard)
- Add auto-checkpoint.sh (Stop event, git stash automation) - Add typecheck-on-save.sh (PostToolUse, TypeScript validation) - Add test-on-change.sh (PostToolUse, smart test detection) - Add file-guard.sh (PreToolUse, unified file protection) - Add ClaudeKit evaluation (3/5, patterns extracted) - Version bump 3.21.0 → 3.21.1 (sync across all docs) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6910c06981
commit
975b8019ac
8 changed files with 948 additions and 9 deletions
38
examples/hooks/bash/auto-checkpoint.sh
Executable file
38
examples/hooks/bash/auto-checkpoint.sh
Executable file
|
|
@ -0,0 +1,38 @@
|
|||
#!/bin/bash
|
||||
# .claude/hooks/auto-checkpoint.sh
|
||||
# Event: Stop
|
||||
# Auto-creates git stash checkpoint when Claude Code session ends
|
||||
# Inspired by checkpoint pattern for safe experimentation
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
# Extract session metadata
|
||||
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
|
||||
TIMESTAMP=$(date +"%Y%m%d-%H%M%S")
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
||||
|
||||
# Check if there are uncommitted changes
|
||||
if ! git diff-index --quiet HEAD -- 2>/dev/null; then
|
||||
# Create descriptive stash name
|
||||
STASH_NAME="claude-checkpoint-${BRANCH}-${TIMESTAMP}"
|
||||
|
||||
# Stash with descriptive message
|
||||
git stash push -u -m "$STASH_NAME" >/dev/null 2>&1
|
||||
|
||||
# Log checkpoint creation
|
||||
LOG_DIR="$HOME/.claude/logs"
|
||||
mkdir -p "$LOG_DIR"
|
||||
echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] Created checkpoint: $STASH_NAME (session: $SESSION_ID)" \
|
||||
>> "$LOG_DIR/checkpoints.log"
|
||||
|
||||
# Notify user
|
||||
cat << EOF
|
||||
{
|
||||
"systemMessage": "✓ Checkpoint created: $STASH_NAME\n\nRestore with:\n git stash list # find the checkpoint\n git stash apply stash@{N} # restore it"
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
exit 0
|
||||
113
examples/hooks/bash/file-guard.sh
Executable file
113
examples/hooks/bash/file-guard.sh
Executable file
|
|
@ -0,0 +1,113 @@
|
|||
#!/bin/bash
|
||||
# .claude/hooks/file-guard.sh
|
||||
# Event: PreToolUse
|
||||
# Unified file protection with pattern matching and bash bypass detection
|
||||
# Prevents Claude from reading/writing protected files
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
|
||||
|
||||
# Only check file operations
|
||||
if [[ "$TOOL_NAME" != "Read" && "$TOOL_NAME" != "Write" && "$TOOL_NAME" != "Edit" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
||||
|
||||
# Load protection patterns from .agentignore or .aiignore
|
||||
IGNORE_FILE=""
|
||||
if [[ -f ".agentignore" ]]; then
|
||||
IGNORE_FILE=".agentignore"
|
||||
elif [[ -f ".aiignore" ]]; then
|
||||
IGNORE_FILE=".aiignore"
|
||||
fi
|
||||
|
||||
# Default critical patterns (if no ignore file)
|
||||
CRITICAL_PATTERNS=(
|
||||
".env"
|
||||
".env.local"
|
||||
".env.production"
|
||||
"*.key"
|
||||
"*.pem"
|
||||
"*.p12"
|
||||
"credentials.json"
|
||||
"secrets.yaml"
|
||||
"config/secrets/*"
|
||||
".aws/credentials"
|
||||
".ssh/id_*"
|
||||
)
|
||||
|
||||
# Check against patterns
|
||||
is_protected() {
|
||||
local file="$1"
|
||||
|
||||
# Check ignore file patterns
|
||||
if [[ -n "$IGNORE_FILE" ]]; then
|
||||
while IFS= read -r pattern; do
|
||||
# Skip comments and empty lines
|
||||
[[ "$pattern" =~ ^#.*$ || -z "$pattern" ]] && continue
|
||||
|
||||
# Convert gitignore pattern to bash glob
|
||||
if [[ "$file" == $pattern || "$file" =~ $pattern ]]; then
|
||||
return 0
|
||||
fi
|
||||
done < "$IGNORE_FILE"
|
||||
fi
|
||||
|
||||
# Check critical patterns
|
||||
for pattern in "${CRITICAL_PATTERNS[@]}"; do
|
||||
if [[ "$file" == $pattern || "$file" =~ $pattern ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Detect bash variable expansion bypass attempts
|
||||
detect_bypass() {
|
||||
local file="$1"
|
||||
|
||||
# Check for variable expansion patterns
|
||||
if [[ "$file" =~ \$\{?[A-Za-z_][A-Za-z0-9_]*\}? ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check for command substitution
|
||||
if [[ "$file" =~ \$\( || "$file" =~ \` ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Validate file path
|
||||
if [[ -z "$FILE_PATH" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check for bypass attempts
|
||||
if detect_bypass "$FILE_PATH"; then
|
||||
cat << EOF
|
||||
{
|
||||
"block": true,
|
||||
"systemMessage": "⛔ File access blocked: Variable expansion detected in path\n\nPath: $FILE_PATH\n\nThis looks like a bypass attempt. Use literal paths only."
|
||||
}
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check protection patterns
|
||||
if is_protected "$FILE_PATH"; then
|
||||
cat << EOF
|
||||
{
|
||||
"block": true,
|
||||
"systemMessage": "⛔ File access blocked: Protected file\n\nPath: $FILE_PATH\n\nThis file is protected by .agentignore or security policy.\nTo access it, remove from ignore file and confirm manually."
|
||||
}
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
73
examples/hooks/bash/test-on-change.sh
Executable file
73
examples/hooks/bash/test-on-change.sh
Executable file
|
|
@ -0,0 +1,73 @@
|
|||
#!/bin/bash
|
||||
# .claude/hooks/test-on-change.sh
|
||||
# Event: PostToolUse
|
||||
# Detects and runs associated tests after code changes
|
||||
# Part of validation pipeline pattern
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
|
||||
|
||||
# Only run after Edit/Write operations
|
||||
if [[ "$TOOL_NAME" != "Edit" && "$TOOL_NAME" != "Write" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
||||
|
||||
# Skip if not a code file
|
||||
if [[ ! "$FILE_PATH" =~ \.(ts|tsx|js|jsx|py|go|rs)$ ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Find associated test file
|
||||
TEST_FILE=""
|
||||
BASENAME=$(basename "$FILE_PATH" | sed 's/\.[^.]*$//')
|
||||
DIRNAME=$(dirname "$FILE_PATH")
|
||||
|
||||
# Common test patterns
|
||||
for pattern in "${BASENAME}.test.ts" "${BASENAME}.test.js" "${BASENAME}_test.py" "${BASENAME}_test.go"; do
|
||||
if [[ -f "$DIRNAME/$pattern" ]]; then
|
||||
TEST_FILE="$DIRNAME/$pattern"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Try adjacent __tests__ directory
|
||||
if [[ -z "$TEST_FILE" ]]; then
|
||||
for pattern in "__tests__/${BASENAME}.test.ts" "__tests__/${BASENAME}.test.js"; do
|
||||
if [[ -f "$DIRNAME/$pattern" ]]; then
|
||||
TEST_FILE="$DIRNAME/$pattern"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# If test file found, run it
|
||||
if [[ -n "$TEST_FILE" ]]; then
|
||||
# Determine test runner
|
||||
if [[ -f "package.json" ]]; then
|
||||
TEST_CMD="npm test -- $TEST_FILE"
|
||||
elif [[ -f "pytest.ini" || -f "pyproject.toml" ]]; then
|
||||
TEST_CMD="pytest $TEST_FILE"
|
||||
elif [[ -f "go.mod" ]]; then
|
||||
TEST_CMD="go test $(dirname $TEST_FILE)"
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Run tests
|
||||
TEST_OUTPUT=$($TEST_CMD 2>&1 || true)
|
||||
|
||||
# Check for failures
|
||||
if echo "$TEST_OUTPUT" | grep -qE "(FAIL|failed|error|Error)"; then
|
||||
cat << EOF
|
||||
{
|
||||
"systemMessage": "⚠ Tests failed in $TEST_FILE:\n\n$TEST_OUTPUT\n\nFix implementation or update tests."
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
41
examples/hooks/bash/typecheck-on-save.sh
Executable file
41
examples/hooks/bash/typecheck-on-save.sh
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
#!/bin/bash
|
||||
# .claude/hooks/typecheck-on-save.sh
|
||||
# Event: PostToolUse
|
||||
# Runs TypeScript compiler on changed files after edits
|
||||
# Part of validation pipeline pattern
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
|
||||
|
||||
# Only run after Edit/Write operations
|
||||
if [[ "$TOOL_NAME" != "Edit" && "$TOOL_NAME" != "Write" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
||||
|
||||
# Only check TypeScript files
|
||||
if [[ ! "$FILE_PATH" =~ \.(ts|tsx)$ ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if tsconfig exists
|
||||
if [[ ! -f "tsconfig.json" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Run tsc on the specific file (noEmit mode)
|
||||
TSC_OUTPUT=$(npx tsc --noEmit "$FILE_PATH" 2>&1 || true)
|
||||
|
||||
# Check if there are errors (not just warnings)
|
||||
if echo "$TSC_OUTPUT" | grep -q "error TS"; then
|
||||
cat << EOF
|
||||
{
|
||||
"systemMessage": "⚠ TypeScript errors in $FILE_PATH:\n\n$TSC_OUTPUT\n\nFix these before proceeding."
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Loading…
Add table
Add a link
Reference in a new issue