sequant 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +237 -0
- package/dist/bin/cli.d.ts +8 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +70 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/src/commands/doctor.d.ts +8 -0
- package/dist/src/commands/doctor.d.ts.map +1 -0
- package/dist/src/commands/doctor.js +171 -0
- package/dist/src/commands/doctor.js.map +1 -0
- package/dist/src/commands/init.d.ts +11 -0
- package/dist/src/commands/init.d.ts.map +1 -0
- package/dist/src/commands/init.js +124 -0
- package/dist/src/commands/init.js.map +1 -0
- package/dist/src/commands/run.d.ts +18 -0
- package/dist/src/commands/run.d.ts.map +1 -0
- package/dist/src/commands/run.js +229 -0
- package/dist/src/commands/run.js.map +1 -0
- package/dist/src/commands/status.d.ts +5 -0
- package/dist/src/commands/status.d.ts.map +1 -0
- package/dist/src/commands/status.js +45 -0
- package/dist/src/commands/status.js.map +1 -0
- package/dist/src/commands/update.d.ts +10 -0
- package/dist/src/commands/update.d.ts.map +1 -0
- package/dist/src/commands/update.js +124 -0
- package/dist/src/commands/update.js.map +1 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +13 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lib/fs.d.ts +10 -0
- package/dist/src/lib/fs.d.ts.map +1 -0
- package/dist/src/lib/fs.js +44 -0
- package/dist/src/lib/fs.js.map +1 -0
- package/dist/src/lib/manifest.d.ts +14 -0
- package/dist/src/lib/manifest.d.ts.map +1 -0
- package/dist/src/lib/manifest.js +37 -0
- package/dist/src/lib/manifest.js.map +1 -0
- package/dist/src/lib/stacks.d.ts +22 -0
- package/dist/src/lib/stacks.d.ts.map +1 -0
- package/dist/src/lib/stacks.js +131 -0
- package/dist/src/lib/stacks.js.map +1 -0
- package/dist/src/lib/templates.d.ts +16 -0
- package/dist/src/lib/templates.d.ts.map +1 -0
- package/dist/src/lib/templates.js +118 -0
- package/dist/src/lib/templates.js.map +1 -0
- package/dist/src/lib/workflow/cli-args.d.ts +138 -0
- package/dist/src/lib/workflow/cli-args.d.ts.map +1 -0
- package/dist/src/lib/workflow/cli-args.js +210 -0
- package/dist/src/lib/workflow/cli-args.js.map +1 -0
- package/dist/src/lib/workflow/execute-issues.d.ts +42 -0
- package/dist/src/lib/workflow/execute-issues.d.ts.map +1 -0
- package/dist/src/lib/workflow/execute-issues.js +463 -0
- package/dist/src/lib/workflow/execute-issues.js.map +1 -0
- package/dist/src/lib/workflow/logger.d.ts +168 -0
- package/dist/src/lib/workflow/logger.d.ts.map +1 -0
- package/dist/src/lib/workflow/logger.js +249 -0
- package/dist/src/lib/workflow/logger.js.map +1 -0
- package/dist/src/lib/workflow/types.d.ts +89 -0
- package/dist/src/lib/workflow/types.d.ts.map +1 -0
- package/dist/src/lib/workflow/types.js +23 -0
- package/dist/src/lib/workflow/types.js.map +1 -0
- package/package.json +69 -0
- package/stacks/go.yaml +22 -0
- package/stacks/nextjs.yaml +28 -0
- package/stacks/python.yaml +24 -0
- package/stacks/rust.yaml +23 -0
- package/templates/hooks/post-tool.sh +301 -0
- package/templates/hooks/pre-tool.sh +350 -0
- package/templates/memory/constitution.md +60 -0
- package/templates/scripts/cleanup-worktree.sh +78 -0
- package/templates/scripts/list-worktrees.sh +50 -0
- package/templates/scripts/new-feature.sh +156 -0
- package/templates/settings.json +26 -0
- package/templates/skills/assess/SKILL.md +428 -0
- package/templates/skills/clean/SKILL.md +196 -0
- package/templates/skills/docs/SKILL.md +323 -0
- package/templates/skills/exec/SKILL.md +426 -0
- package/templates/skills/fullsolve/SKILL.md +479 -0
- package/templates/skills/loop/SKILL.md +310 -0
- package/templates/skills/qa/SKILL.md +261 -0
- package/templates/skills/qa/references/code-quality-exemplars.md +112 -0
- package/templates/skills/qa/references/code-review-checklist.md +77 -0
- package/templates/skills/qa/references/quality-gates.md +95 -0
- package/templates/skills/qa/references/testing-requirements.md +109 -0
- package/templates/skills/qa/scripts/quality-checks.sh +109 -0
- package/templates/skills/reflect/SKILL.md +159 -0
- package/templates/skills/reflect/references/documentation-tiers.md +70 -0
- package/templates/skills/reflect/references/phase-reflection.md +95 -0
- package/templates/skills/reflect/scripts/workflow-queries.ts +165 -0
- package/templates/skills/security-review/SKILL.md +344 -0
- package/templates/skills/security-review/references/security-checklists.md +377 -0
- package/templates/skills/solve/SKILL.md +242 -0
- package/templates/skills/spec/SKILL.md +169 -0
- package/templates/skills/spec/references/parallel-groups.md +72 -0
- package/templates/skills/spec/references/verification-criteria.md +104 -0
- package/templates/skills/test/SKILL.md +508 -0
- package/templates/skills/testgen/SKILL.md +561 -0
- package/templates/skills/verify/SKILL.md +266 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Post-tool hook for Claude Code
|
|
3
|
+
# - Timing instrumentation (END timestamp to pair with pre-tool START)
|
|
4
|
+
# - Auto-formatting for code quality
|
|
5
|
+
# - Quality observability (test/build failures, SQL queries)
|
|
6
|
+
# - Smart test running (P3): Runs related tests after file edits (opt-in)
|
|
7
|
+
# - Webhook notifications (P3): Notifies on issue close (opt-in)
|
|
8
|
+
# Runs AFTER each tool completes
|
|
9
|
+
|
|
10
|
+
# === ROLLBACK MECHANISM ===
|
|
11
|
+
# Set CLAUDE_HOOKS_DISABLED=true to bypass all hook logic
|
|
12
|
+
if [[ "${CLAUDE_HOOKS_DISABLED:-}" == "true" ]]; then
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# === READ INPUT FROM STDIN ===
|
|
17
|
+
# Claude Code passes tool data as JSON via stdin, not environment variables
|
|
18
|
+
INPUT_JSON=$(cat)
|
|
19
|
+
|
|
20
|
+
# Parse JSON using jq (preferred) or fallback to grep
|
|
21
|
+
if command -v jq &>/dev/null; then
|
|
22
|
+
TOOL_NAME=$(echo "$INPUT_JSON" | jq -r '.tool_name // empty')
|
|
23
|
+
# For Bash tool, extract .command from tool_input; for others, stringify the whole object
|
|
24
|
+
if [[ "$(echo "$INPUT_JSON" | jq -r '.tool_name // empty')" == "Bash" ]]; then
|
|
25
|
+
TOOL_INPUT=$(echo "$INPUT_JSON" | jq -r '.tool_input.command // empty')
|
|
26
|
+
else
|
|
27
|
+
TOOL_INPUT=$(echo "$INPUT_JSON" | jq -r '.tool_input | tostring // empty')
|
|
28
|
+
fi
|
|
29
|
+
TOOL_OUTPUT=$(echo "$INPUT_JSON" | jq -r '.tool_response | tostring // empty')
|
|
30
|
+
else
|
|
31
|
+
TOOL_NAME=$(echo "$INPUT_JSON" | grep -oE '"tool_name"\s*:\s*"[^"]+"' | head -1 | cut -d'"' -f4)
|
|
32
|
+
# For Bash tool, extract command from tool_input; for others, extract the whole object
|
|
33
|
+
if [[ "$TOOL_NAME" == "Bash" ]]; then
|
|
34
|
+
TOOL_INPUT=$(echo "$INPUT_JSON" | grep -oE '"command"\s*:\s*"[^"]+"' | head -1 | cut -d'"' -f4)
|
|
35
|
+
else
|
|
36
|
+
TOOL_INPUT=$(echo "$INPUT_JSON" | grep -oE '"tool_input"\s*:\s*\{[^}]+\}' | head -1)
|
|
37
|
+
fi
|
|
38
|
+
TOOL_OUTPUT=$(echo "$INPUT_JSON" | grep -oE '"tool_response"\s*:\s*\{[^}]+\}' | head -1)
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
TIMING_LOG="/tmp/claude-timing.log"
|
|
42
|
+
QUALITY_LOG="/tmp/claude-quality.log"
|
|
43
|
+
TESTS_LOG="/tmp/claude-tests.log"
|
|
44
|
+
PARALLEL_MARKER_PREFIX="/tmp/claude-parallel-"
|
|
45
|
+
|
|
46
|
+
# === AGENT ID DETECTION ===
|
|
47
|
+
# For parallel agents, detect group ID from marker files
|
|
48
|
+
# Format: /tmp/claude-parallel-<group-id>.marker
|
|
49
|
+
AGENT_ID=""
|
|
50
|
+
IS_PARALLEL_AGENT="false"
|
|
51
|
+
for marker in "${PARALLEL_MARKER_PREFIX}"*.marker; do
|
|
52
|
+
if [[ -f "$marker" ]]; then
|
|
53
|
+
# Extract group ID from marker filename
|
|
54
|
+
AGENT_ID=$(basename "$marker" | sed 's/claude-parallel-//' | sed 's/\.marker//')
|
|
55
|
+
IS_PARALLEL_AGENT="true"
|
|
56
|
+
break
|
|
57
|
+
fi
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
# === TIMING END ===
|
|
61
|
+
# Include agent ID in log format if available (AC-4)
|
|
62
|
+
if [[ -n "$AGENT_ID" ]]; then
|
|
63
|
+
echo "$(date +%s.%N) [$AGENT_ID] END $TOOL_NAME" >> "$TIMING_LOG"
|
|
64
|
+
else
|
|
65
|
+
echo "$(date +%s.%N) END $TOOL_NAME" >> "$TIMING_LOG"
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# === LOG ROTATION FOR QUALITY LOG ===
|
|
69
|
+
# Rotate if over 1000 lines to prevent unbounded growth
|
|
70
|
+
if [[ -f "$QUALITY_LOG" ]]; then
|
|
71
|
+
LINE_COUNT=$(wc -l < "$QUALITY_LOG" 2>/dev/null || echo 0)
|
|
72
|
+
if [[ "$LINE_COUNT" -gt 1000 ]]; then
|
|
73
|
+
tail -500 "$QUALITY_LOG" > "${QUALITY_LOG}.tmp" && mv "${QUALITY_LOG}.tmp" "$QUALITY_LOG"
|
|
74
|
+
fi
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# === LOG ROTATION FOR TESTS LOG ===
|
|
78
|
+
if [[ -f "$TESTS_LOG" ]]; then
|
|
79
|
+
LINE_COUNT=$(wc -l < "$TESTS_LOG" 2>/dev/null || echo 0)
|
|
80
|
+
if [[ "$LINE_COUNT" -gt 1000 ]]; then
|
|
81
|
+
tail -500 "$TESTS_LOG" > "${TESTS_LOG}.tmp" && mv "${TESTS_LOG}.tmp" "$TESTS_LOG"
|
|
82
|
+
fi
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# === JSON PARSING HELPER ===
|
|
86
|
+
# Try jq first for reliable JSON parsing, fall back to grep for simpler systems
|
|
87
|
+
extract_file_path() {
|
|
88
|
+
local input="$1"
|
|
89
|
+
local path=""
|
|
90
|
+
|
|
91
|
+
if command -v jq &>/dev/null; then
|
|
92
|
+
path=$(echo "$input" | jq -r '.file_path // empty' 2>/dev/null)
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# Fallback to grep if jq fails or isn't available
|
|
96
|
+
if [[ -z "$path" ]]; then
|
|
97
|
+
path=$(echo "$input" | grep -oE '"file_path"\s*:\s*"[^"]+"' | head -1 | cut -d'"' -f4)
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
echo "$path"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# === SECURITY WARNING LOGGING (AC-3 for Issue #492) ===
|
|
104
|
+
# Log warnings (don't block) for dangerous patterns in edited/written files
|
|
105
|
+
# These are not blocking because there may be legitimate uses, but should be reviewed
|
|
106
|
+
check_security_patterns() {
|
|
107
|
+
local content="$1"
|
|
108
|
+
local file_path="$2"
|
|
109
|
+
local warnings=()
|
|
110
|
+
|
|
111
|
+
# dangerouslyDisableSandbox usage (Bash tool security bypass)
|
|
112
|
+
if echo "$content" | grep -qE 'dangerouslyDisableSandbox.*true'; then
|
|
113
|
+
warnings+=("dangerouslyDisableSandbox=true (Bash security bypass)")
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# eval() usage (dynamic code execution - XSS/injection risk)
|
|
117
|
+
if echo "$content" | grep -qE '\beval\s*\('; then
|
|
118
|
+
warnings+=("eval() usage (dynamic code execution)")
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# innerHTML assignment (XSS vulnerability without sanitization)
|
|
122
|
+
if echo "$content" | grep -qE '\.innerHTML\s*='; then
|
|
123
|
+
warnings+=("innerHTML assignment (potential XSS)")
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# SQL string concatenation (SQL injection risk)
|
|
127
|
+
# Look for patterns like: query + variable or `SELECT ... ${variable}`
|
|
128
|
+
if echo "$content" | grep -qE "(query|sql|SQL)\s*\+\s*|query\s*=.*\\\$\{"; then
|
|
129
|
+
warnings+=("SQL string concatenation (potential injection)")
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
# Log any warnings found
|
|
133
|
+
for warning in "${warnings[@]}"; do
|
|
134
|
+
echo "$(date +%H:%M:%S) SECURITY_WARNING: $warning in $file_path" >> "$QUALITY_LOG"
|
|
135
|
+
done
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if [[ "${CLAUDE_HOOKS_SECURITY:-true}" != "false" ]]; then
|
|
139
|
+
if [[ "$TOOL_NAME" == "Edit" || "$TOOL_NAME" == "Write" ]]; then
|
|
140
|
+
FILE_PATH=$(extract_file_path "$TOOL_INPUT")
|
|
141
|
+
|
|
142
|
+
if [[ -n "$FILE_PATH" && -f "$FILE_PATH" ]]; then
|
|
143
|
+
# Only check TypeScript/JavaScript files for security patterns
|
|
144
|
+
if [[ "$FILE_PATH" =~ \.(ts|tsx|js|jsx)$ ]]; then
|
|
145
|
+
FILE_CONTENT=$(cat "$FILE_PATH" 2>/dev/null || true)
|
|
146
|
+
if [[ -n "$FILE_CONTENT" ]]; then
|
|
147
|
+
check_security_patterns "$FILE_CONTENT" "$FILE_PATH"
|
|
148
|
+
fi
|
|
149
|
+
fi
|
|
150
|
+
fi
|
|
151
|
+
fi
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# === AUTO-FORMAT ON FILE WRITE ===
|
|
155
|
+
# Skip auto-formatting for parallel agents (AC-5)
|
|
156
|
+
# Parent agent will format after the parallel group completes
|
|
157
|
+
if [[ "$IS_PARALLEL_AGENT" == "true" ]]; then
|
|
158
|
+
# Log that formatting was skipped for parallel agent
|
|
159
|
+
if [[ "$TOOL_NAME" == "Edit" || "$TOOL_NAME" == "Write" ]]; then
|
|
160
|
+
FILE_PATH=$(extract_file_path "$TOOL_INPUT")
|
|
161
|
+
if [[ -n "$FILE_PATH" ]]; then
|
|
162
|
+
echo "$(date +%H:%M:%S) SKIP_FORMAT (parallel): $FILE_PATH" >> "$QUALITY_LOG"
|
|
163
|
+
fi
|
|
164
|
+
fi
|
|
165
|
+
elif [[ "$TOOL_NAME" == "Edit" || "$TOOL_NAME" == "Write" ]]; then
|
|
166
|
+
FILE_PATH=$(extract_file_path "$TOOL_INPUT")
|
|
167
|
+
|
|
168
|
+
if [[ -n "$FILE_PATH" && -f "$FILE_PATH" ]]; then
|
|
169
|
+
# Auto-format TypeScript/JavaScript files (synchronous to avoid race conditions)
|
|
170
|
+
if [[ "$FILE_PATH" =~ \.(ts|tsx|js|jsx)$ ]]; then
|
|
171
|
+
if npx prettier --write "$FILE_PATH" 2>/dev/null; then
|
|
172
|
+
echo "$(date +%H:%M:%S) FORMATTED: $FILE_PATH" >> "$QUALITY_LOG"
|
|
173
|
+
fi
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
# Auto-format JSON files (synchronous)
|
|
177
|
+
if [[ "$FILE_PATH" =~ \.json$ ]]; then
|
|
178
|
+
npx prettier --write "$FILE_PATH" 2>/dev/null
|
|
179
|
+
fi
|
|
180
|
+
fi
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
# === LOG SUPABASE QUERIES ===
|
|
184
|
+
if [[ "$TOOL_NAME" == "mcp__supabase__execute_sql" ]]; then
|
|
185
|
+
# Extract SQL query with jq or fallback
|
|
186
|
+
if command -v jq &>/dev/null; then
|
|
187
|
+
QUERY=$(echo "$TOOL_INPUT" | jq -r '.query // empty' 2>/dev/null | head -c 200)
|
|
188
|
+
else
|
|
189
|
+
QUERY=$(echo "$TOOL_INPUT" | head -c 200)
|
|
190
|
+
fi
|
|
191
|
+
echo "$(date +%H:%M:%S) SQL: $QUERY" >> "$QUALITY_LOG"
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
# === TRACK GIT OPERATIONS ===
|
|
195
|
+
if [[ "$TOOL_NAME" == "Bash" ]]; then
|
|
196
|
+
if echo "$TOOL_INPUT" | grep -qE 'git (commit|push|pr create)'; then
|
|
197
|
+
# Truncate long git commands for readability
|
|
198
|
+
GIT_CMD=$(echo "$TOOL_INPUT" | head -c 200)
|
|
199
|
+
echo "$(date +%H:%M:%S) GIT: $GIT_CMD" >> "$QUALITY_LOG"
|
|
200
|
+
fi
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
# === DETECT TEST FAILURES ===
|
|
204
|
+
if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'npm (test|run test)'; then
|
|
205
|
+
if echo "$TOOL_OUTPUT" | grep -qE '(FAIL|failed|Error:)'; then
|
|
206
|
+
echo "$(date +%H:%M:%S) TEST_FAILURE detected" >> "$QUALITY_LOG"
|
|
207
|
+
fi
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
# === DETECT BUILD FAILURES ===
|
|
211
|
+
if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'npm run build'; then
|
|
212
|
+
if echo "$TOOL_OUTPUT" | grep -qE '(error TS|Build failed|Error:)'; then
|
|
213
|
+
echo "$(date +%H:%M:%S) BUILD_FAILURE detected" >> "$QUALITY_LOG"
|
|
214
|
+
fi
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
# === SMART TEST RUNNING (P3) ===
|
|
218
|
+
# Opt-in: Set CLAUDE_HOOKS_SMART_TESTS=true to enable
|
|
219
|
+
# Runs related tests asynchronously after file edits
|
|
220
|
+
if [[ "${CLAUDE_HOOKS_SMART_TESTS:-}" == "true" ]]; then
|
|
221
|
+
if [[ "$TOOL_NAME" == "Edit" || "$TOOL_NAME" == "Write" ]]; then
|
|
222
|
+
FILE_PATH=$(extract_file_path "$TOOL_INPUT")
|
|
223
|
+
|
|
224
|
+
if [[ -n "$FILE_PATH" && "$FILE_PATH" =~ \.(ts|tsx)$ ]]; then
|
|
225
|
+
# Extract filename without extension (use -E for macOS sed compatibility)
|
|
226
|
+
FILENAME=$(basename "$FILE_PATH" | sed -E 's/\.(ts|tsx)$//')
|
|
227
|
+
|
|
228
|
+
# Find related test file in __tests__/ directory
|
|
229
|
+
# This project uses centralized tests, not co-located
|
|
230
|
+
PROJECT_ROOT="${FILE_PATH%%/lib/*}"
|
|
231
|
+
if [[ "$PROJECT_ROOT" == "$FILE_PATH" ]]; then
|
|
232
|
+
PROJECT_ROOT="${FILE_PATH%%/components/*}"
|
|
233
|
+
fi
|
|
234
|
+
if [[ "$PROJECT_ROOT" == "$FILE_PATH" ]]; then
|
|
235
|
+
PROJECT_ROOT="${FILE_PATH%%/app/*}"
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
# Search for test files matching the source file name
|
|
239
|
+
TEST_FILE=""
|
|
240
|
+
if [[ -d "$PROJECT_ROOT/__tests__" ]]; then
|
|
241
|
+
# Try direct match first
|
|
242
|
+
if [[ -f "$PROJECT_ROOT/__tests__/${FILENAME}.test.ts" ]]; then
|
|
243
|
+
TEST_FILE="$PROJECT_ROOT/__tests__/${FILENAME}.test.ts"
|
|
244
|
+
elif [[ -f "$PROJECT_ROOT/__tests__/${FILENAME}.test.tsx" ]]; then
|
|
245
|
+
TEST_FILE="$PROJECT_ROOT/__tests__/${FILENAME}.test.tsx"
|
|
246
|
+
# Try integration tests
|
|
247
|
+
elif [[ -f "$PROJECT_ROOT/__tests__/integration/${FILENAME}.test.ts" ]]; then
|
|
248
|
+
TEST_FILE="$PROJECT_ROOT/__tests__/integration/${FILENAME}.test.ts"
|
|
249
|
+
elif [[ -f "$PROJECT_ROOT/__tests__/integration/${FILENAME}.test.tsx" ]]; then
|
|
250
|
+
TEST_FILE="$PROJECT_ROOT/__tests__/integration/${FILENAME}.test.tsx"
|
|
251
|
+
fi
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
if [[ -n "$TEST_FILE" && -f "$TEST_FILE" ]]; then
|
|
255
|
+
echo "$(date +%H:%M:%S) SMART_TEST: Running $TEST_FILE for $FILE_PATH" >> "$TESTS_LOG"
|
|
256
|
+
|
|
257
|
+
# Run test asynchronously to avoid blocking
|
|
258
|
+
# Use timeout/gtimeout if available, otherwise run without timeout
|
|
259
|
+
(
|
|
260
|
+
cd "$PROJECT_ROOT" 2>/dev/null || exit
|
|
261
|
+
TIMEOUT_CMD=""
|
|
262
|
+
if command -v timeout &>/dev/null; then
|
|
263
|
+
TIMEOUT_CMD="timeout 30"
|
|
264
|
+
elif command -v gtimeout &>/dev/null; then
|
|
265
|
+
TIMEOUT_CMD="gtimeout 30"
|
|
266
|
+
fi
|
|
267
|
+
$TIMEOUT_CMD npm test -- --testPathPatterns="$(basename "$TEST_FILE")" --silent 2>&1 | head -20 >> "$TESTS_LOG"
|
|
268
|
+
if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
|
|
269
|
+
echo "$(date +%H:%M:%S) SMART_TEST_RESULT: FAIL" >> "$TESTS_LOG"
|
|
270
|
+
else
|
|
271
|
+
echo "$(date +%H:%M:%S) SMART_TEST_RESULT: PASS" >> "$TESTS_LOG"
|
|
272
|
+
fi
|
|
273
|
+
) &
|
|
274
|
+
fi
|
|
275
|
+
fi
|
|
276
|
+
fi
|
|
277
|
+
fi
|
|
278
|
+
|
|
279
|
+
# === WEBHOOK NOTIFICATIONS (P3) ===
|
|
280
|
+
# Opt-in: Set CLAUDE_HOOKS_WEBHOOK_URL to enable
|
|
281
|
+
# Fires notification when issues are closed
|
|
282
|
+
if [[ -n "${CLAUDE_HOOKS_WEBHOOK_URL:-}" ]]; then
|
|
283
|
+
if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'gh issue close'; then
|
|
284
|
+
# Extract issue number
|
|
285
|
+
ISSUE_NUM=$(echo "$TOOL_INPUT" | grep -oE '#?[0-9]+' | head -1 | tr -d '#')
|
|
286
|
+
|
|
287
|
+
if [[ -n "$ISSUE_NUM" ]]; then
|
|
288
|
+
echo "$(date +%H:%M:%S) WEBHOOK: Notifying issue #$ISSUE_NUM closed" >> "$QUALITY_LOG"
|
|
289
|
+
|
|
290
|
+
# Fire-and-forget async curl (don't block on webhook failures)
|
|
291
|
+
(
|
|
292
|
+
curl -s -X POST "$CLAUDE_HOOKS_WEBHOOK_URL" \
|
|
293
|
+
-H 'Content-Type: application/json' \
|
|
294
|
+
-d "{\"text\":\"Issue #$ISSUE_NUM completed by Claude Code automation\"}" \
|
|
295
|
+
--max-time 5 2>/dev/null || true
|
|
296
|
+
) &
|
|
297
|
+
fi
|
|
298
|
+
fi
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
exit 0
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Pre-tool hook for Claude Code
|
|
3
|
+
# - Security guardrails for execute-issues.ts (blocks catastrophic commands)
|
|
4
|
+
# - Timing instrumentation for performance analysis
|
|
5
|
+
# Exit 0 = allow, Exit 2 = block (Exit 1 = non-blocking error, logged but not blocked)
|
|
6
|
+
|
|
7
|
+
# === ROLLBACK MECHANISM ===
|
|
8
|
+
# Set CLAUDE_HOOKS_DISABLED=true to bypass all hook logic
|
|
9
|
+
if [[ "${CLAUDE_HOOKS_DISABLED:-}" == "true" ]]; then
|
|
10
|
+
exit 0
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
# === READ INPUT FROM STDIN ===
|
|
14
|
+
# Claude Code passes tool data as JSON via stdin, not environment variables
|
|
15
|
+
INPUT_JSON=$(cat)
|
|
16
|
+
|
|
17
|
+
# Parse JSON using jq (preferred) or fallback to grep
|
|
18
|
+
if command -v jq &>/dev/null; then
|
|
19
|
+
TOOL_NAME=$(echo "$INPUT_JSON" | jq -r '.tool_name // empty')
|
|
20
|
+
# For Bash tool, extract .command from tool_input; for others, stringify the whole object
|
|
21
|
+
if [[ "$(echo "$INPUT_JSON" | jq -r '.tool_name // empty')" == "Bash" ]]; then
|
|
22
|
+
TOOL_INPUT=$(echo "$INPUT_JSON" | jq -r '.tool_input.command // empty')
|
|
23
|
+
else
|
|
24
|
+
TOOL_INPUT=$(echo "$INPUT_JSON" | jq -r '.tool_input | tostring // empty')
|
|
25
|
+
fi
|
|
26
|
+
else
|
|
27
|
+
TOOL_NAME=$(echo "$INPUT_JSON" | grep -oE '"tool_name"\s*:\s*"[^"]+"' | head -1 | cut -d'"' -f4)
|
|
28
|
+
# For Bash tool, extract command from tool_input; for others, extract the whole object
|
|
29
|
+
if [[ "$TOOL_NAME" == "Bash" ]]; then
|
|
30
|
+
TOOL_INPUT=$(echo "$INPUT_JSON" | grep -oE '"command"\s*:\s*"[^"]+"' | head -1 | cut -d'"' -f4)
|
|
31
|
+
else
|
|
32
|
+
TOOL_INPUT=$(echo "$INPUT_JSON" | grep -oE '"tool_input"\s*:\s*\{[^}]+\}' | head -1)
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
TIMING_LOG="/tmp/claude-timing.log"
|
|
37
|
+
PARALLEL_MARKER_PREFIX="/tmp/claude-parallel-"
|
|
38
|
+
|
|
39
|
+
# === AGENT ID DETECTION ===
|
|
40
|
+
# For parallel agents, detect group ID from marker files
|
|
41
|
+
# Format: /tmp/claude-parallel-<group-id>.marker
|
|
42
|
+
AGENT_ID=""
|
|
43
|
+
for marker in "${PARALLEL_MARKER_PREFIX}"*.marker; do
|
|
44
|
+
if [[ -f "$marker" ]]; then
|
|
45
|
+
# Extract group ID from marker filename
|
|
46
|
+
AGENT_ID=$(basename "$marker" | sed 's/claude-parallel-//' | sed 's/\.marker//')
|
|
47
|
+
break
|
|
48
|
+
fi
|
|
49
|
+
done
|
|
50
|
+
|
|
51
|
+
# === TIMING START ===
|
|
52
|
+
# Include agent ID in log format if available (AC-4)
|
|
53
|
+
if [[ -n "$AGENT_ID" ]]; then
|
|
54
|
+
echo "$(date +%s.%N) [$AGENT_ID] START $TOOL_NAME" >> "$TIMING_LOG"
|
|
55
|
+
else
|
|
56
|
+
echo "$(date +%s.%N) START $TOOL_NAME" >> "$TIMING_LOG"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# === LOG ROTATION ===
|
|
60
|
+
# Rotate if over 1000 lines to prevent unbounded growth
|
|
61
|
+
if [[ -f "$TIMING_LOG" ]]; then
|
|
62
|
+
LINE_COUNT=$(wc -l < "$TIMING_LOG" 2>/dev/null || echo 0)
|
|
63
|
+
if [[ "$LINE_COUNT" -gt 1000 ]]; then
|
|
64
|
+
tail -500 "$TIMING_LOG" > "${TIMING_LOG}.tmp" && mv "${TIMING_LOG}.tmp" "$TIMING_LOG"
|
|
65
|
+
fi
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# === CATASTROPHIC BLOCKS ===
|
|
69
|
+
# These should NEVER run in any automated context
|
|
70
|
+
|
|
71
|
+
# Secrets/credentials
|
|
72
|
+
# Skip check for gh commands (comment/pr bodies may contain example text)
|
|
73
|
+
if ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) '; then
|
|
74
|
+
# Pattern requires command to START with file reader (not match in quoted strings)
|
|
75
|
+
if echo "$TOOL_INPUT" | grep -qE '^(cat|less|head|tail|more) .*\.(env|pem|key)'; then
|
|
76
|
+
echo "HOOK_BLOCKED: Reading secret file" | tee -a /tmp/claude-hook.log >&2
|
|
77
|
+
exit 2
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
if echo "$TOOL_INPUT" | grep -qE '^(cat|less) .*~/\.(ssh|aws|gnupg|config/gh)'; then
|
|
81
|
+
echo "HOOK_BLOCKED: Reading credential directory" | tee -a /tmp/claude-hook.log >&2
|
|
82
|
+
exit 2
|
|
83
|
+
fi
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
# Bare environment dump
|
|
87
|
+
if echo "$TOOL_INPUT" | grep -qE '^(env|printenv|export)$'; then
|
|
88
|
+
echo "HOOK_BLOCKED: Environment dump" | tee -a /tmp/claude-hook.log >&2
|
|
89
|
+
exit 2
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# Destructive system commands
|
|
93
|
+
if echo "$TOOL_INPUT" | grep -qE 'sudo|rm -rf /|rm -rf ~|rm -rf \$HOME'; then
|
|
94
|
+
echo "HOOK_BLOCKED: Destructive system command" | tee -a /tmp/claude-hook.log >&2
|
|
95
|
+
exit 2
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# Deployment (should never happen in issue automation)
|
|
99
|
+
if echo "$TOOL_INPUT" | grep -qE 'vercel (deploy|--prod)|terraform (apply|destroy)|kubectl (apply|delete)'; then
|
|
100
|
+
echo "HOOK_BLOCKED: Deployment command" | tee -a /tmp/claude-hook.log >&2
|
|
101
|
+
exit 2
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# Force push
|
|
105
|
+
# Pattern requires -f to be a standalone flag (not part of branch name like -fix)
|
|
106
|
+
if echo "$TOOL_INPUT" | grep -qE 'git push.*(--force| -f($| ))'; then
|
|
107
|
+
echo "HOOK_BLOCKED: Force push" | tee -a /tmp/claude-hook.log >&2
|
|
108
|
+
exit 2
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# CI/CD triggers (automation shouldn't trigger more automation)
|
|
112
|
+
if echo "$TOOL_INPUT" | grep -qE 'gh workflow run'; then
|
|
113
|
+
echo "HOOK_BLOCKED: Workflow trigger" | tee -a /tmp/claude-hook.log >&2
|
|
114
|
+
exit 2
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# === SECURITY GUARDRAILS ===
|
|
118
|
+
# Granular disable: Set CLAUDE_HOOKS_SECURITY=false to bypass security checks only
|
|
119
|
+
# (separate from CLAUDE_HOOKS_DISABLED which bypasses ALL hooks)
|
|
120
|
+
|
|
121
|
+
# --- Secret Detection (AC-1 for Issue #492) ---
|
|
122
|
+
# Block commits containing hardcoded API keys, tokens, and secrets
|
|
123
|
+
check_secrets() {
|
|
124
|
+
local content="$1"
|
|
125
|
+
local patterns=(
|
|
126
|
+
'sk-[a-zA-Z0-9]{32,}' # OpenAI API key
|
|
127
|
+
'sk_live_[a-zA-Z0-9]{24,}' # Stripe live key
|
|
128
|
+
'AKIA[A-Z0-9]{16}' # AWS Access Key
|
|
129
|
+
'ghp_[a-zA-Z0-9]{36}' # GitHub Personal Token
|
|
130
|
+
'xoxb-[0-9]{10,}(-[a-zA-Z0-9]+)+' # Slack Bot Token
|
|
131
|
+
'AIza[a-zA-Z0-9_-]{35}' # Google API Key
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
for pattern in "${patterns[@]}"; do
|
|
135
|
+
if echo "$content" | grep -qE "$pattern"; then
|
|
136
|
+
return 0 # Found a secret
|
|
137
|
+
fi
|
|
138
|
+
done
|
|
139
|
+
return 1 # No secrets found
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# --- Sensitive File Detection (AC-2 for Issue #492) ---
|
|
143
|
+
# Block commits containing sensitive files
|
|
144
|
+
check_sensitive_files() {
|
|
145
|
+
local files="$1"
|
|
146
|
+
local patterns=(
|
|
147
|
+
'\.env$'
|
|
148
|
+
'\.env\.local$'
|
|
149
|
+
'\.env\.production$'
|
|
150
|
+
'\.env\.[^.]+$' # Any .env.* file
|
|
151
|
+
'credentials\.json$'
|
|
152
|
+
'\.pem$'
|
|
153
|
+
'\.key$'
|
|
154
|
+
'id_rsa$'
|
|
155
|
+
'id_ed25519$'
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
for pattern in "${patterns[@]}"; do
|
|
159
|
+
if echo "$files" | grep -qE "$pattern"; then
|
|
160
|
+
return 0 # Found sensitive file
|
|
161
|
+
fi
|
|
162
|
+
done
|
|
163
|
+
return 1 # No sensitive files found
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if [[ "${CLAUDE_HOOKS_SECURITY:-true}" != "false" ]]; then
|
|
167
|
+
# Security checks for git commit
|
|
168
|
+
if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
|
|
169
|
+
# Skip security checks if --no-verify is used
|
|
170
|
+
if ! echo "$TOOL_INPUT" | grep -qE -- '--no-verify'; then
|
|
171
|
+
# Check staged files for secrets
|
|
172
|
+
STAGED_CONTENT=$(git diff --cached 2>/dev/null || true)
|
|
173
|
+
if [[ -n "$STAGED_CONTENT" ]] && check_secrets "$STAGED_CONTENT"; then
|
|
174
|
+
{
|
|
175
|
+
echo "HOOK_BLOCKED: Hardcoded secret detected in staged changes"
|
|
176
|
+
echo " Use 'git commit --no-verify' to bypass if this is a false positive"
|
|
177
|
+
} | tee -a /tmp/claude-hook.log >&2
|
|
178
|
+
exit 2
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
# Check for sensitive files in commit
|
|
182
|
+
STAGED_FILES=$(git diff --cached --name-only 2>/dev/null || true)
|
|
183
|
+
if [[ -n "$STAGED_FILES" ]] && check_sensitive_files "$STAGED_FILES"; then
|
|
184
|
+
{
|
|
185
|
+
echo "HOOK_BLOCKED: Sensitive file in commit (${STAGED_FILES})"
|
|
186
|
+
echo " Files like .env, *.pem, *.key should not be committed"
|
|
187
|
+
echo " Use 'git commit --no-verify' to bypass if this is intentional"
|
|
188
|
+
} | tee -a /tmp/claude-hook.log >&2
|
|
189
|
+
exit 2
|
|
190
|
+
fi
|
|
191
|
+
fi
|
|
192
|
+
fi
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
# === QUALITY GUARDS (Phase 2) ===
|
|
196
|
+
|
|
197
|
+
# --- No-Changes Guard (AC-7) ---
|
|
198
|
+
# Block commits when there are no staged or unstaged changes (prevents empty commits)
|
|
199
|
+
# Skips for --amend since amending doesn't require new changes
|
|
200
|
+
if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
|
|
201
|
+
if ! echo "$TOOL_INPUT" | grep -qE -- '--amend|--allow-empty'; then
|
|
202
|
+
# Check for changes (staged or unstaged)
|
|
203
|
+
CHANGES=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
|
204
|
+
if [[ "$CHANGES" -eq 0 ]]; then
|
|
205
|
+
echo "HOOK_BLOCKED: No changes to commit. Stage files with 'git add' first." | tee -a /tmp/claude-hook.log >&2
|
|
206
|
+
exit 2
|
|
207
|
+
fi
|
|
208
|
+
fi
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
# --- Worktree Validation (AC-8) ---
|
|
212
|
+
# Warn (but don't block) when committing outside a feature worktree
|
|
213
|
+
# This catches accidental commits to main repo during feature work
|
|
214
|
+
QUALITY_LOG="/tmp/claude-quality.log"
|
|
215
|
+
if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
|
|
216
|
+
CWD=$(pwd)
|
|
217
|
+
if ! echo "$CWD" | grep -qE 'worktrees/feature/'; then
|
|
218
|
+
echo "$(date +%H:%M:%S) WORKTREE_WARNING: Committing outside feature worktree ($CWD)" >> "$QUALITY_LOG"
|
|
219
|
+
# Warning only - does not block
|
|
220
|
+
fi
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
# --- Commit Message Validation (AC-3) ---
|
|
224
|
+
# Enforce conventional commits format: type(scope): description
|
|
225
|
+
# Types: feat|fix|docs|style|refactor|test|chore|ci|build|perf
|
|
226
|
+
if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
|
|
227
|
+
# Extract message from -m flag
|
|
228
|
+
MSG=""
|
|
229
|
+
|
|
230
|
+
# Try heredoc format first: -m "$(cat <<'EOF' ... EOF)"
|
|
231
|
+
# This is the most common format in Claude Code git commits
|
|
232
|
+
if echo "$TOOL_INPUT" | grep -qE "<<.*EOF"; then
|
|
233
|
+
# Extract first line after heredoc marker
|
|
234
|
+
MSG=$(echo "$TOOL_INPUT" | sed -n '/<<.*EOF/,/EOF/p' | sed '1d;$d' | head -1 | sed 's/^[[:space:]]*//')
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
# Try -m "message" format (double quotes)
|
|
238
|
+
if [[ -z "$MSG" ]] && echo "$TOOL_INPUT" | grep -qE '\-m\s+"'; then
|
|
239
|
+
MSG=$(echo "$TOOL_INPUT" | awk -F'"' '{print $2}')
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
# Try -m 'message' format (single quotes)
|
|
243
|
+
if [[ -z "$MSG" ]] && echo "$TOOL_INPUT" | grep -qE "\-m\s+'"; then
|
|
244
|
+
MSG=$(echo "$TOOL_INPUT" | awk -F"'" '{print $2}')
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
# Validate if we found a message
|
|
248
|
+
if [[ -n "$MSG" ]]; then
|
|
249
|
+
# Conventional commits pattern: type(optional-scope): description
|
|
250
|
+
# Also accepts ! for breaking changes: feat!: or feat(scope)!:
|
|
251
|
+
PATTERN='^(feat|fix|docs|style|refactor|test|chore|ci|build|perf)(\([^)]+\))?(!)?\s*:'
|
|
252
|
+
if ! echo "$MSG" | grep -qE "$PATTERN"; then
|
|
253
|
+
{
|
|
254
|
+
echo "HOOK_BLOCKED: Commit must follow conventional commits format"
|
|
255
|
+
echo " Expected: type(scope): description"
|
|
256
|
+
echo " Types: feat|fix|docs|style|refactor|test|chore|ci|build|perf"
|
|
257
|
+
echo " Got: $MSG"
|
|
258
|
+
} | tee -a /tmp/claude-hook.log >&2
|
|
259
|
+
exit 2
|
|
260
|
+
fi
|
|
261
|
+
fi
|
|
262
|
+
fi
|
|
263
|
+
|
|
264
|
+
# === WORKTREE PATH ENFORCEMENT FOR PARALLEL AGENTS ===
|
|
265
|
+
# When a parallel marker exists with a worktree path, block edits outside that worktree
|
|
266
|
+
# This prevents agents from accidentally editing the main repo instead of the worktree
|
|
267
|
+
# Marker file format: First line contains the expected worktree path
|
|
268
|
+
if [[ "$TOOL_NAME" == "Edit" || "$TOOL_NAME" == "Write" ]]; then
|
|
269
|
+
EXPECTED_WORKTREE=""
|
|
270
|
+
for marker in "${PARALLEL_MARKER_PREFIX}"*.marker; do
|
|
271
|
+
if [[ -f "$marker" ]]; then
|
|
272
|
+
# Read expected worktree path from marker file (first line)
|
|
273
|
+
EXPECTED_WORKTREE=$(head -1 "$marker" 2>/dev/null || true)
|
|
274
|
+
break
|
|
275
|
+
fi
|
|
276
|
+
done
|
|
277
|
+
|
|
278
|
+
if [[ -n "$EXPECTED_WORKTREE" ]]; then
|
|
279
|
+
# AC-1 (Issue #550): Check worktree directory exists before path validation
|
|
280
|
+
# Prevents Write tool from creating non-existent worktree directories
|
|
281
|
+
if [[ ! -d "$EXPECTED_WORKTREE" ]]; then
|
|
282
|
+
echo "HOOK_BLOCKED: Worktree does not exist: $EXPECTED_WORKTREE" | tee -a /tmp/claude-hook.log >&2
|
|
283
|
+
exit 2
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
FILE_PATH=""
|
|
287
|
+
if command -v jq &>/dev/null; then
|
|
288
|
+
FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // empty' 2>/dev/null)
|
|
289
|
+
fi
|
|
290
|
+
if [[ -z "$FILE_PATH" ]]; then
|
|
291
|
+
FILE_PATH=$(echo "$TOOL_INPUT" | grep -oE '"file_path"\s*:\s*"[^"]+"' | head -1 | cut -d'"' -f4)
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
if [[ -n "$FILE_PATH" ]]; then
|
|
295
|
+
# Check if file path is within the expected worktree
|
|
296
|
+
if ! echo "$FILE_PATH" | grep -qF "$EXPECTED_WORKTREE"; then
|
|
297
|
+
echo "$(date +%H:%M:%S) WORKTREE_BLOCKED: Edit outside expected worktree" >> "$QUALITY_LOG"
|
|
298
|
+
echo " Expected: $EXPECTED_WORKTREE" >> "$QUALITY_LOG"
|
|
299
|
+
echo " Got: $FILE_PATH" >> "$QUALITY_LOG"
|
|
300
|
+
echo "HOOK_BLOCKED: Edit must be in worktree: $EXPECTED_WORKTREE (got: $FILE_PATH)" | tee -a /tmp/claude-hook.log >&2
|
|
301
|
+
exit 2
|
|
302
|
+
fi
|
|
303
|
+
fi
|
|
304
|
+
fi
|
|
305
|
+
fi
|
|
306
|
+
|
|
307
|
+
# === FILE LOCKING FOR PARALLEL AGENTS (AC-6) ===
|
|
308
|
+
# Prevents concurrent edits to the same file when parallel agents are running
|
|
309
|
+
# Uses lockf (macOS native) with a per-file lock in /tmp
|
|
310
|
+
# Disabled with CLAUDE_HOOKS_FILE_LOCKING=false
|
|
311
|
+
if [[ "${CLAUDE_HOOKS_FILE_LOCKING:-true}" == "true" ]]; then
|
|
312
|
+
if [[ "$TOOL_NAME" == "Edit" || "$TOOL_NAME" == "Write" ]]; then
|
|
313
|
+
FILE_PATH=""
|
|
314
|
+
if command -v jq &>/dev/null; then
|
|
315
|
+
FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // empty' 2>/dev/null)
|
|
316
|
+
fi
|
|
317
|
+
if [[ -z "$FILE_PATH" ]]; then
|
|
318
|
+
FILE_PATH=$(echo "$TOOL_INPUT" | grep -oE '"file_path"\s*:\s*"[^"]+"' | head -1 | cut -d'"' -f4)
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
if [[ -n "$FILE_PATH" ]]; then
|
|
322
|
+
# Create a lock file based on file path hash (handles special chars)
|
|
323
|
+
LOCK_FILE="/tmp/claude-lock-$(echo "$FILE_PATH" | md5 -q 2>/dev/null || echo "$FILE_PATH" | md5sum | cut -d' ' -f1).lock"
|
|
324
|
+
|
|
325
|
+
# Try to acquire lock with 30 second timeout
|
|
326
|
+
# Use a subshell to hold the lock during the tool execution
|
|
327
|
+
if command -v lockf &>/dev/null; then
|
|
328
|
+
# macOS: use lockf
|
|
329
|
+
exec 200>"$LOCK_FILE"
|
|
330
|
+
if ! lockf -t 30 200 2>/dev/null; then
|
|
331
|
+
echo "HOOK_BLOCKED: File locked by another agent: $FILE_PATH" | tee -a /tmp/claude-hook.log >&2
|
|
332
|
+
exit 2
|
|
333
|
+
fi
|
|
334
|
+
# Lock will be released when the file descriptor closes (process exits)
|
|
335
|
+
elif command -v flock &>/dev/null; then
|
|
336
|
+
# Linux: use flock
|
|
337
|
+
exec 200>"$LOCK_FILE"
|
|
338
|
+
if ! flock -w 30 200 2>/dev/null; then
|
|
339
|
+
echo "HOOK_BLOCKED: File locked by another agent: $FILE_PATH" | tee -a /tmp/claude-hook.log >&2
|
|
340
|
+
exit 2
|
|
341
|
+
fi
|
|
342
|
+
fi
|
|
343
|
+
# If neither lockf nor flock available, proceed without locking
|
|
344
|
+
fi
|
|
345
|
+
fi
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
# === ALLOW EVERYTHING ELSE ===
|
|
349
|
+
# Slash commands need: git, npm, file edits, gh pr/issue, supabase queries
|
|
350
|
+
exit 0
|