-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathpre-commit.sh
More file actions
93 lines (78 loc) · 3.09 KB
/
Copy pathpre-commit.sh
File metadata and controls
93 lines (78 loc) · 3.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#!/usr/bin/env bash
# pre-commit.sh — PreToolUse hook for Bash (git commit)
# Thin wrapper that runs all codegraph pre-commit checks in a single
# Node.js process: cycles (blocking), dead exports (blocking),
# signature warnings (informational), diff-impact (informational).
set -euo pipefail
INPUT=$(cat)
# Extract the command from tool_input JSON
COMMAND=$(echo "$INPUT" | node -e "
let d='';
process.stdin.on('data',c=>d+=c);
process.stdin.on('end',()=>{
const p=JSON.parse(d).tool_input?.command||'';
if(p)process.stdout.write(p);
});
" 2>/dev/null) || true
if [ -z "$COMMAND" ]; then
exit 0
fi
# Only trigger on git commit commands
if ! echo "$COMMAND" | grep -qE '(^|\s|&&\s*)git\s+commit\b'; then
exit 0
fi
# Guard: codegraph DB must exist
WORK_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || WORK_ROOT="${CLAUDE_PROJECT_DIR:-.}"
if [ ! -f "$WORK_ROOT/.codegraph/graph.db" ]; then
exit 0
fi
# Guard: must have staged changes
STAGED=$(git diff --cached --name-only 2>/dev/null) || true
if [ -z "$STAGED" ]; then
exit 0
fi
# Load session edit log
LOG_FILE="$WORK_ROOT/.claude/session-edits.log"
EDITED_FILES=""
if [ -f "$LOG_FILE" ] && [ -s "$LOG_FILE" ]; then
EDITED_FILES=$(awk '{print $2}' "$LOG_FILE" | sort -u)
fi
# Run all checks in a single Node.js process
HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
STRIP_FLAG=$(node -e "const [M,m]=process.versions.node.split('.').map(Number); console.log(M>=23?'--strip-types':'--experimental-strip-types')")
RESULT=$(node $STRIP_FLAG "$HOOK_DIR/pre-commit-checks.ts" "$WORK_ROOT" "$EDITED_FILES" "$STAGED" 2>/dev/null) || true
if [ -z "$RESULT" ]; then
exit 0
fi
# Parse action
ACTION=$(echo "$RESULT" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{process.stdout.write(JSON.parse(d).action||'allow')}catch{process.stdout.write('allow')}})" 2>/dev/null) || ACTION="allow"
if [ "$ACTION" = "deny" ]; then
REASON=$(echo "$RESULT" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{process.stdout.write(JSON.parse(d).reason||'')}catch{}})" 2>/dev/null) || true
node -e "
console.log(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason: process.argv[1]
}
}));
" "$REASON"
exit 0
fi
# Inject informational context (non-blocking)
CONTEXT=$(echo "$RESULT" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const c=JSON.parse(d).context||[];if(c.length)process.stdout.write(c.join('\n\n'))}catch{}})" 2>/dev/null) || true
if [ -n "$CONTEXT" ]; then
ESCAPED=$(printf '%s' "$CONTEXT" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>process.stdout.write(JSON.stringify(d)))" 2>/dev/null) || true
if [ -n "$ESCAPED" ]; then
node -e "
console.log(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'allow',
additionalContext: JSON.parse(process.argv[1])
}
}));
" "$ESCAPED" 2>/dev/null || true
fi
fi
exit 0