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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +237 -0
  3. package/dist/bin/cli.d.ts +8 -0
  4. package/dist/bin/cli.d.ts.map +1 -0
  5. package/dist/bin/cli.js +70 -0
  6. package/dist/bin/cli.js.map +1 -0
  7. package/dist/src/commands/doctor.d.ts +8 -0
  8. package/dist/src/commands/doctor.d.ts.map +1 -0
  9. package/dist/src/commands/doctor.js +171 -0
  10. package/dist/src/commands/doctor.js.map +1 -0
  11. package/dist/src/commands/init.d.ts +11 -0
  12. package/dist/src/commands/init.d.ts.map +1 -0
  13. package/dist/src/commands/init.js +124 -0
  14. package/dist/src/commands/init.js.map +1 -0
  15. package/dist/src/commands/run.d.ts +18 -0
  16. package/dist/src/commands/run.d.ts.map +1 -0
  17. package/dist/src/commands/run.js +229 -0
  18. package/dist/src/commands/run.js.map +1 -0
  19. package/dist/src/commands/status.d.ts +5 -0
  20. package/dist/src/commands/status.d.ts.map +1 -0
  21. package/dist/src/commands/status.js +45 -0
  22. package/dist/src/commands/status.js.map +1 -0
  23. package/dist/src/commands/update.d.ts +10 -0
  24. package/dist/src/commands/update.d.ts.map +1 -0
  25. package/dist/src/commands/update.js +124 -0
  26. package/dist/src/commands/update.js.map +1 -0
  27. package/dist/src/index.d.ts +15 -0
  28. package/dist/src/index.d.ts.map +1 -0
  29. package/dist/src/index.js +13 -0
  30. package/dist/src/index.js.map +1 -0
  31. package/dist/src/lib/fs.d.ts +10 -0
  32. package/dist/src/lib/fs.d.ts.map +1 -0
  33. package/dist/src/lib/fs.js +44 -0
  34. package/dist/src/lib/fs.js.map +1 -0
  35. package/dist/src/lib/manifest.d.ts +14 -0
  36. package/dist/src/lib/manifest.d.ts.map +1 -0
  37. package/dist/src/lib/manifest.js +37 -0
  38. package/dist/src/lib/manifest.js.map +1 -0
  39. package/dist/src/lib/stacks.d.ts +22 -0
  40. package/dist/src/lib/stacks.d.ts.map +1 -0
  41. package/dist/src/lib/stacks.js +131 -0
  42. package/dist/src/lib/stacks.js.map +1 -0
  43. package/dist/src/lib/templates.d.ts +16 -0
  44. package/dist/src/lib/templates.d.ts.map +1 -0
  45. package/dist/src/lib/templates.js +118 -0
  46. package/dist/src/lib/templates.js.map +1 -0
  47. package/dist/src/lib/workflow/cli-args.d.ts +138 -0
  48. package/dist/src/lib/workflow/cli-args.d.ts.map +1 -0
  49. package/dist/src/lib/workflow/cli-args.js +210 -0
  50. package/dist/src/lib/workflow/cli-args.js.map +1 -0
  51. package/dist/src/lib/workflow/execute-issues.d.ts +42 -0
  52. package/dist/src/lib/workflow/execute-issues.d.ts.map +1 -0
  53. package/dist/src/lib/workflow/execute-issues.js +463 -0
  54. package/dist/src/lib/workflow/execute-issues.js.map +1 -0
  55. package/dist/src/lib/workflow/logger.d.ts +168 -0
  56. package/dist/src/lib/workflow/logger.d.ts.map +1 -0
  57. package/dist/src/lib/workflow/logger.js +249 -0
  58. package/dist/src/lib/workflow/logger.js.map +1 -0
  59. package/dist/src/lib/workflow/types.d.ts +89 -0
  60. package/dist/src/lib/workflow/types.d.ts.map +1 -0
  61. package/dist/src/lib/workflow/types.js +23 -0
  62. package/dist/src/lib/workflow/types.js.map +1 -0
  63. package/package.json +69 -0
  64. package/stacks/go.yaml +22 -0
  65. package/stacks/nextjs.yaml +28 -0
  66. package/stacks/python.yaml +24 -0
  67. package/stacks/rust.yaml +23 -0
  68. package/templates/hooks/post-tool.sh +301 -0
  69. package/templates/hooks/pre-tool.sh +350 -0
  70. package/templates/memory/constitution.md +60 -0
  71. package/templates/scripts/cleanup-worktree.sh +78 -0
  72. package/templates/scripts/list-worktrees.sh +50 -0
  73. package/templates/scripts/new-feature.sh +156 -0
  74. package/templates/settings.json +26 -0
  75. package/templates/skills/assess/SKILL.md +428 -0
  76. package/templates/skills/clean/SKILL.md +196 -0
  77. package/templates/skills/docs/SKILL.md +323 -0
  78. package/templates/skills/exec/SKILL.md +426 -0
  79. package/templates/skills/fullsolve/SKILL.md +479 -0
  80. package/templates/skills/loop/SKILL.md +310 -0
  81. package/templates/skills/qa/SKILL.md +261 -0
  82. package/templates/skills/qa/references/code-quality-exemplars.md +112 -0
  83. package/templates/skills/qa/references/code-review-checklist.md +77 -0
  84. package/templates/skills/qa/references/quality-gates.md +95 -0
  85. package/templates/skills/qa/references/testing-requirements.md +109 -0
  86. package/templates/skills/qa/scripts/quality-checks.sh +109 -0
  87. package/templates/skills/reflect/SKILL.md +159 -0
  88. package/templates/skills/reflect/references/documentation-tiers.md +70 -0
  89. package/templates/skills/reflect/references/phase-reflection.md +95 -0
  90. package/templates/skills/reflect/scripts/workflow-queries.ts +165 -0
  91. package/templates/skills/security-review/SKILL.md +344 -0
  92. package/templates/skills/security-review/references/security-checklists.md +377 -0
  93. package/templates/skills/solve/SKILL.md +242 -0
  94. package/templates/skills/spec/SKILL.md +169 -0
  95. package/templates/skills/spec/references/parallel-groups.md +72 -0
  96. package/templates/skills/spec/references/verification-criteria.md +104 -0
  97. package/templates/skills/test/SKILL.md +508 -0
  98. package/templates/skills/testgen/SKILL.md +561 -0
  99. 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