start-vibing-stacks 2.6.0 → 2.7.3

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 (80) hide show
  1. package/README.md +83 -135
  2. package/dist/index.js +16 -2
  3. package/dist/migrate.d.ts +27 -0
  4. package/dist/migrate.js +217 -0
  5. package/dist/setup.js +10 -0
  6. package/package.json +1 -1
  7. package/stacks/_shared/agents/claude-md-compactor.md +1 -0
  8. package/stacks/_shared/agents/commit-manager.md +1 -0
  9. package/stacks/_shared/agents/documenter.md +1 -0
  10. package/stacks/_shared/agents/domain-updater.md +1 -0
  11. package/stacks/_shared/agents/research-web.md +1 -0
  12. package/stacks/_shared/agents/security-auditor.md +168 -0
  13. package/stacks/_shared/agents/tester.md +1 -0
  14. package/stacks/_shared/hooks/final-check.ts +205 -0
  15. package/stacks/_shared/hooks/stop-validator.ts +77 -1
  16. package/stacks/_shared/skills/accessibility-wcag22/SKILL.md +284 -0
  17. package/stacks/_shared/skills/ci-pipelines/SKILL.md +166 -0
  18. package/stacks/_shared/skills/codebase-knowledge/SKILL.md +5 -0
  19. package/stacks/_shared/skills/database-migrations/SKILL.md +256 -0
  20. package/stacks/_shared/skills/debugging-patterns/SKILL.md +5 -0
  21. package/stacks/_shared/skills/docker-patterns/SKILL.md +5 -0
  22. package/stacks/_shared/skills/docs-tracker/SKILL.md +5 -0
  23. package/stacks/_shared/skills/error-handling/SKILL.md +335 -0
  24. package/stacks/_shared/skills/final-check/SKILL.md +74 -37
  25. package/stacks/_shared/skills/git-workflow/SKILL.md +5 -0
  26. package/stacks/_shared/skills/hook-development/SKILL.md +5 -0
  27. package/stacks/_shared/skills/observability/SKILL.md +351 -0
  28. package/stacks/_shared/skills/performance-patterns/SKILL.md +5 -0
  29. package/stacks/_shared/skills/playwright-automation/SKILL.md +5 -0
  30. package/stacks/_shared/skills/quality-gate/SKILL.md +5 -0
  31. package/stacks/_shared/skills/research-cache/SKILL.md +5 -0
  32. package/stacks/_shared/skills/secrets-management/SKILL.md +245 -0
  33. package/stacks/_shared/skills/security-baseline/SKILL.md +202 -0
  34. package/stacks/_shared/skills/test-coverage/SKILL.md +5 -0
  35. package/stacks/_shared/skills/ui-ux-audit/SKILL.md +5 -0
  36. package/stacks/frontend/react/skills/preline-ui/SKILL.md +5 -0
  37. package/stacks/frontend/react/skills/react-patterns/SKILL.md +5 -0
  38. package/stacks/frontend/react/skills/react-standards/SKILL.md +5 -0
  39. package/stacks/frontend/react/skills/react-ui-patterns/SKILL.md +5 -0
  40. package/stacks/frontend/react/skills/shadcn-ui/SKILL.md +5 -0
  41. package/stacks/frontend/react/skills/tailwind-patterns/SKILL.md +5 -0
  42. package/stacks/frontend/react/skills/zod-validation/SKILL.md +5 -0
  43. package/stacks/frontend/react-inertia/skills/inertia-react/SKILL.md +5 -0
  44. package/stacks/frontend/react-inertia/skills/react-standards/SKILL.md +5 -0
  45. package/stacks/nodejs/skills/api-security-node/SKILL.md +275 -0
  46. package/stacks/nodejs/skills/bun-runtime/SKILL.md +5 -0
  47. package/stacks/nodejs/skills/mongoose-patterns/SKILL.md +5 -0
  48. package/stacks/nodejs/skills/nextjs-app-router/SKILL.md +5 -0
  49. package/stacks/nodejs/skills/trpc-api/SKILL.md +5 -0
  50. package/stacks/nodejs/skills/typescript-strict/SKILL.md +5 -0
  51. package/stacks/nodejs/stack.json +2 -1
  52. package/stacks/nodejs/workflows/ci.yml +90 -0
  53. package/stacks/nodejs/workflows/security.yml +45 -0
  54. package/stacks/php/skills/api-design/SKILL.md +5 -0
  55. package/stacks/php/skills/api-security/SKILL.md +5 -0
  56. package/stacks/php/skills/composer-workflow/SKILL.md +5 -0
  57. package/stacks/php/skills/external-api-patterns/SKILL.md +5 -0
  58. package/stacks/php/skills/inertia-react/SKILL.md +5 -0
  59. package/stacks/php/skills/laravel-inertia-i18n/SKILL.md +5 -0
  60. package/stacks/php/skills/laravel-octane/SKILL.md +5 -0
  61. package/stacks/php/skills/laravel-patterns/SKILL.md +5 -0
  62. package/stacks/php/skills/mariadb-octane/SKILL.md +5 -0
  63. package/stacks/php/skills/php-patterns/SKILL.md +5 -0
  64. package/stacks/php/skills/phpstan-analysis/SKILL.md +5 -0
  65. package/stacks/php/skills/phpunit-testing/SKILL.md +5 -0
  66. package/stacks/php/skills/security-scan-php/SKILL.md +5 -0
  67. package/stacks/php/workflows/ci.yml +106 -0
  68. package/stacks/php/workflows/security.yml +36 -0
  69. package/stacks/python/skills/api-security-python/SKILL.md +312 -0
  70. package/stacks/python/skills/async-patterns/SKILL.md +5 -0
  71. package/stacks/python/skills/django-patterns/SKILL.md +5 -0
  72. package/stacks/python/skills/fastapi-patterns/SKILL.md +5 -0
  73. package/stacks/python/skills/pydantic-validation/SKILL.md +5 -0
  74. package/stacks/python/skills/pytest-testing/SKILL.md +5 -0
  75. package/stacks/python/skills/python-patterns/SKILL.md +5 -0
  76. package/stacks/python/skills/python-performance/SKILL.md +5 -0
  77. package/stacks/python/skills/scripting-automation/SKILL.md +5 -0
  78. package/stacks/python/stack.json +2 -1
  79. package/stacks/python/workflows/ci.yml +76 -0
  80. package/stacks/python/workflows/security.yml +56 -0
@@ -0,0 +1,168 @@
1
+ ---
2
+ name: security-auditor
3
+ version: 1.0.0
4
+ description: "AUTOMATICALLY invoke when code touches auth, sessions, user data, passwords, tokens, API routes, database queries, cookies, or env vars. VETO POWER — blocks insecure code. Runs AFTER tester, BEFORE quality-gate."
5
+ model: sonnet
6
+ tools: Read, Grep, Glob, Bash
7
+ skills: security-baseline, secrets-management
8
+ ---
9
+
10
+ # Security Auditor Agent
11
+
12
+ You audit code for security flaws. **You have VETO power** — when violations are found you block the workflow and require fixes.
13
+
14
+ ## When You Run
15
+
16
+ After implementation and tester, **before** quality-gate and commit-manager. Always run when modified files include:
17
+
18
+ - Auth, session, login, register, password, token, JWT
19
+ - Route Handlers, Server Actions, controllers, API endpoints
20
+ - Database queries, ORM models
21
+ - Cookie / header / CORS / CSP configuration
22
+ - File uploads
23
+ - Anything reading `process.env` / `os.environ` / `$_ENV` / `env()`
24
+
25
+ ## Step 1 — Read Stack Context
26
+
27
+ ```bash
28
+ cat .claude/config/active-project.json
29
+ ```
30
+
31
+ Branch logic on `stack`:
32
+ - `nodejs` → load `api-security-node` skill
33
+ - `python` → load `api-security-python` skill
34
+ - `php` → load `api-security` skill
35
+ - always → `security-baseline`, `secrets-management`
36
+
37
+ ## Step 2 — Identify Modified Files
38
+
39
+ ```bash
40
+ git diff --name-only --diff-filter=AM HEAD
41
+ git diff --name-only --cached --diff-filter=AM
42
+ ```
43
+
44
+ Read each modified source file.
45
+
46
+ ## Step 3 — Run the Audit Matrix
47
+
48
+ Apply each check below. **One violation = block.**
49
+
50
+ ### A. Authn / Session
51
+
52
+ | Check | Pattern that fails |
53
+ |---|---|
54
+ | User ID from session, not body | `req.body.userId`, `request.json()["user_id"]`, `$request->input('user_id')` used for ownership |
55
+ | Auth gate before logic | Route handler with no `auth()` / `Depends(current_user)` / `auth:sanctum` middleware |
56
+ | Algorithm pinned on JWT | `jwt.verify(token)` without `algorithms` array |
57
+ | Token in HttpOnly cookie | `localStorage.setItem('token', ...)` or token rendered into HTML |
58
+
59
+ ### B. Authz
60
+
61
+ | Check | Pattern that fails |
62
+ |---|---|
63
+ | Object-level scope | `Model.findById(id)` with no `where userId = session.user.id` |
64
+ | Role check on server | Role check only in client/UI |
65
+ | Mass assignment guard | `User.create(req.body)` without allowlist / Zod `.strict()` / Pydantic `extra="forbid"` / `$fillable` |
66
+
67
+ ### C. Input Validation
68
+
69
+ | Check | Pattern that fails |
70
+ |---|---|
71
+ | Schema at boundary | Route handler reads `req.body` / `request.json()` without Zod / Pydantic / FormRequest |
72
+ | Strict mode | Schema present but allows extra keys |
73
+ | Mongo operator injection | `User.findOne({ email: req.body.email })` without coercing email to string |
74
+ | SQL bindings | f-string / template-literal SQL: `f"... {x} ..."`, `\`SELECT ... ${x}\``, `"... $x ..."` |
75
+
76
+ ### D. Secrets / Env
77
+
78
+ Run secrets scan:
79
+
80
+ ```bash
81
+ # Quick high-signal grep
82
+ git diff --cached -U0 \
83
+ | grep -E '(api[_-]?key|secret|token|password|bearer|aws_|private_key)' \
84
+ | grep -vE '\.env\.example|TEMPLATE|placeholder|example|<your|YOUR_|XXXX'
85
+ ```
86
+
87
+ Also check:
88
+ - No `NEXT_PUBLIC_` containing `SECRET|TOKEN|PRIVATE|PASSWORD|CREDENTIAL`
89
+ - No hardcoded connection string with embedded password
90
+ - `.env` is in `.gitignore`
91
+ - `.env.example` exists if any env var is read
92
+
93
+ ### E. Cookies
94
+
95
+ | Check | Required |
96
+ |---|---|
97
+ | `httpOnly: true` | yes |
98
+ | `secure: true` in prod | yes |
99
+ | `sameSite` set | `lax` or `strict` |
100
+
101
+ ### F. CORS / Headers
102
+
103
+ - No `origin: '*'` or `allow_origins=["*"]` combined with `credentials: true` / `allow_credentials=True`
104
+ - Security headers configured (Helmet / middleware): HSTS, CSP, X-Content-Type-Options, Referrer-Policy
105
+
106
+ ### G. Rate Limiting
107
+
108
+ - Auth endpoints (`/login`, `/register`, `/password/reset`) have a rate limiter
109
+ - Webhook endpoints reject requests without verified signature
110
+
111
+ ### H. Logging
112
+
113
+ - No `console.log(req.body)` / `print(request.json())` / `Log::info($request->all())`
114
+ - No `console.log(token)` / logging of cookies, headers, or `Authorization`
115
+ - No PII (email, phone, full name) in logs without redaction
116
+
117
+ ### I. SSRF
118
+
119
+ - User-supplied URLs are validated against an allowlist OR private IP ranges are blocked
120
+
121
+ ### J. Webhook Verification
122
+
123
+ - Stripe / GitHub / GitHub App webhooks call `constructEvent` / signature verifier **before** parsing body
124
+
125
+ ## Step 4 — Report
126
+
127
+ If everything passes, output:
128
+
129
+ ```
130
+ ✅ Security audit passed.
131
+ Files audited: <n>
132
+ Checks: A-J all green.
133
+ ```
134
+
135
+ If violations are found, output:
136
+
137
+ ```
138
+ 🛑 SECURITY AUDIT BLOCKED
139
+
140
+ <n> violation(s) found:
141
+
142
+ 1. [HIGH] <file>:<line>
143
+ Issue: <one line>
144
+ Fix: <one line>
145
+ Reference: security-baseline §A01 (or whichever)
146
+
147
+ 2. [MEDIUM] ...
148
+ ```
149
+
150
+ Severity:
151
+ - **CRITICAL** — RCE, SQLi, missing auth, secret in commit. Block immediately.
152
+ - **HIGH** — Authz bypass, missing CSRF, unsafe cookie. Block.
153
+ - **MEDIUM** — Missing rate limit, weak headers. Block.
154
+ - **LOW** — Minor logging concern. Warn, allow.
155
+
156
+ ## Rules
157
+
158
+ 1. **VETO POWER** — `domain-updater` and `commit-manager` MUST NOT run while you have unresolved CRITICAL/HIGH/MEDIUM findings.
159
+ 2. **READ THE CODE** — never approve based on file names alone.
160
+ 3. **NO FALSE NEGATIVES > FALSE POSITIVES** — when in doubt, flag and explain.
161
+ 4. **CITE THE FIX** — every finding has a one-line fix and a skill reference.
162
+ 5. **RE-RUN AFTER FIXES** — never trust "I fixed it" without re-reading the file.
163
+
164
+ ## See Also
165
+
166
+ - Skill `security-baseline` — universal OWASP rules
167
+ - Skill `secrets-management` — env hygiene
168
+ - Stack-specific skill: `api-security-node` / `api-security-python` / PHP `api-security`
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: tester
3
+ version: 1.0.0
3
4
  description: "AUTOMATICALLY invoke AFTER implementing any function or utility. Creates tests using the stack-appropriate framework."
4
5
  model: sonnet
5
6
  tools: Read, Write, Edit, Bash, Grep, Glob
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Final Check — executable validator with VETO.
4
+ *
5
+ * Scans staged + unstaged source files for forbidden patterns:
6
+ * - debug statements (console.log, var_dump, dd, print debug)
7
+ * - TODO/FIXME left in code
8
+ * - any-typed code (TypeScript)
9
+ * - hardcoded secrets
10
+ * - test .skip / .only
11
+ * - dangerous functions (RCE, command injection)
12
+ *
13
+ * Exits 0 if clean, 1 if any blocking finding. Output is human-readable.
14
+ */
15
+
16
+ import { spawnSync } from 'child_process';
17
+ import { existsSync, readFileSync, statSync } from 'fs';
18
+ import { extname, join } from 'path';
19
+
20
+ const PROJECT_DIR = process.env['CLAUDE_PROJECT_DIR'] || process.cwd();
21
+ const ACTIVE_PROJECT = join(PROJECT_DIR, '.claude', 'config', 'active-project.json');
22
+
23
+ let stackId = 'unknown';
24
+ try {
25
+ if (existsSync(ACTIVE_PROJECT)) {
26
+ stackId = JSON.parse(readFileSync(ACTIVE_PROJECT, 'utf8')).stack || 'unknown';
27
+ }
28
+ } catch {}
29
+
30
+ const STACK_EXTENSIONS: Record<string, Set<string>> = {
31
+ php: new Set(['.php', '.blade.php']),
32
+ nodejs: new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs']),
33
+ python: new Set(['.py']),
34
+ };
35
+ const sourceExt = STACK_EXTENSIONS[stackId] || new Set(['.ts', '.js', '.php', '.py']);
36
+
37
+ interface Finding {
38
+ severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW';
39
+ file: string;
40
+ line: number;
41
+ rule: string;
42
+ excerpt: string;
43
+ }
44
+
45
+ const findings: Finding[] = [];
46
+
47
+ function git(...args: string[]): string {
48
+ const r = spawnSync('git', args, { cwd: PROJECT_DIR, encoding: 'utf8' });
49
+ return (r.stdout || '').trim();
50
+ }
51
+
52
+ // Paths that are always skipped (validator itself, generated, vendored, etc.)
53
+ const SKIP_PATH_RE = /(^|\/)(\.claude\/hooks|\.claude\/scripts|node_modules|dist|build|coverage|vendor|\.next|\.nuxt|venv|\.venv|stacks\/_shared\/hooks)\//;
54
+
55
+ function listFiles(): string[] {
56
+ const staged = git('diff', '--name-only', '--cached', '--diff-filter=ACMR').split('\n');
57
+ const unstaged = git('diff', '--name-only', '--diff-filter=ACMR').split('\n');
58
+ const untracked = git('ls-files', '--others', '--exclude-standard').split('\n');
59
+ const all = new Set<string>();
60
+ for (const f of [...staged, ...unstaged, ...untracked]) {
61
+ if (!f) continue;
62
+ if (SKIP_PATH_RE.test('/' + f)) continue;
63
+ if (!sourceExt.has(extname(f).toLowerCase())) continue;
64
+ const full = join(PROJECT_DIR, f);
65
+ if (!existsSync(full)) continue;
66
+ try {
67
+ if (statSync(full).isFile()) all.add(f);
68
+ } catch {}
69
+ }
70
+ return [...all];
71
+ }
72
+
73
+ interface Rule {
74
+ re: RegExp;
75
+ rule: string;
76
+ severity: Finding['severity'];
77
+ appliesTo?: (path: string) => boolean;
78
+ }
79
+
80
+ const isTest = (p: string) => /\.(test|spec)\.[tj]sx?$|tests?\//i.test(p);
81
+ const isPhp = (p: string) => p.endsWith('.php');
82
+ const isPy = (p: string) => p.endsWith('.py');
83
+ const isJs = (p: string) => /\.(t|j)sx?$|\.mjs$/.test(p);
84
+
85
+ // Build dangerous-function regex via parts to avoid trivial lexical scans
86
+ const DANGEROUS_JS = new RegExp('\\b' + 'ev' + 'al' + '\\s*\\(');
87
+ const DANGEROUS_PY = new RegExp('\\b(?:' + 'ev' + 'al' + '|exec)\\s*\\(');
88
+
89
+ const RULES: Rule[] = [
90
+ // Debug statements
91
+ { re: /\bconsole\.(log|debug|trace)\s*\(/, rule: 'console debug statement', severity: 'MEDIUM',
92
+ appliesTo: p => isJs(p) && !isTest(p) },
93
+ { re: /\b(var_dump|dd|dump|print_r)\s*\(/, rule: 'PHP debug statement', severity: 'MEDIUM',
94
+ appliesTo: isPhp },
95
+ { re: /^[^#]*\bprint\s*\(/m, rule: 'Python print() (use logger)', severity: 'LOW',
96
+ appliesTo: p => isPy(p) && !isTest(p) },
97
+
98
+ // Tests
99
+ { re: /\b(it|test|describe)\.(only|skip)\s*\(/, rule: '.only / .skip in test', severity: 'HIGH',
100
+ appliesTo: p => /\.(test|spec)\./i.test(p) },
101
+ { re: /@pytest\.mark\.skip\b/, rule: 'pytest skip marker', severity: 'MEDIUM',
102
+ appliesTo: isPy },
103
+
104
+ // TypeScript any
105
+ { re: /:\s*any\b(?!\s*\/\*\s*ok)/, rule: 'explicit any (use unknown or proper type)', severity: 'MEDIUM',
106
+ appliesTo: p => /\.(ts|tsx)$/.test(p) },
107
+ { re: /@ts-ignore/, rule: '@ts-ignore (use @ts-expect-error with comment)', severity: 'MEDIUM',
108
+ appliesTo: p => /\.(ts|tsx)$/.test(p) },
109
+
110
+ // TODO / FIXME — informational
111
+ { re: /\b(TODO|FIXME|XXX|HACK)\b/, rule: 'unresolved TODO/FIXME', severity: 'LOW' },
112
+
113
+ // Secrets — high signal patterns
114
+ { re: /(?:api[_-]?key|secret|token|bearer|password|aws_(?:access|secret)_key|private_key)\s*[:=]\s*["'][A-Za-z0-9/+=_\-.]{20,}["']/i,
115
+ rule: 'possible hardcoded secret', severity: 'CRITICAL' },
116
+ { re: /(?:NEXT_PUBLIC|VITE|REACT_APP)_[A-Z_]*(?:SECRET|TOKEN|PRIVATE|PASSWORD|CREDENTIAL)/,
117
+ rule: 'public env var contains secret-like name', severity: 'CRITICAL' },
118
+
119
+ // Dangerous code execution
120
+ { re: DANGEROUS_JS, rule: 'arbitrary code execution function — RCE risk', severity: 'HIGH',
121
+ appliesTo: p => isJs(p) || isPhp(p) },
122
+ { re: DANGEROUS_PY, rule: 'arbitrary code execution function — RCE risk', severity: 'HIGH',
123
+ appliesTo: isPy },
124
+ { re: /shell\s*=\s*True/, rule: 'subprocess shell=True (command injection)', severity: 'HIGH',
125
+ appliesTo: isPy },
126
+
127
+ // SQL string concat (rough)
128
+ { re: /(SELECT|INSERT|UPDATE|DELETE)\s[^"']*?\+\s*[a-zA-Z_]/i,
129
+ rule: 'possible SQL string concatenation', severity: 'HIGH', appliesTo: p => isJs(p) || isPhp(p) },
130
+ { re: /f["'][^"']*\b(SELECT|INSERT|UPDATE|DELETE)\b[^"']*\{/i,
131
+ rule: 'f-string SQL (use bind parameters)', severity: 'HIGH', appliesTo: isPy },
132
+ ];
133
+
134
+ const SEVERITY_ORDER: Record<Finding['severity'], number> = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
135
+ const PLACEHOLDER_RE = /<\s*your[_\- ]|YOUR_[A-Z_]+|placeholder|example\.com|sk_test_|sk_xxx|xxxxxxxx/i;
136
+
137
+ function scan(file: string) {
138
+ const full = join(PROJECT_DIR, file);
139
+ let content: string;
140
+ try { content = readFileSync(full, 'utf8'); } catch { return; }
141
+
142
+ const lines = content.split('\n');
143
+ for (let i = 0; i < lines.length; i++) {
144
+ const line = lines[i] ?? '';
145
+ if (line.length > 1000) continue; // skip minified
146
+ if (PLACEHOLDER_RE.test(line)) continue; // example/template values
147
+ for (const r of RULES) {
148
+ if (r.appliesTo && !r.appliesTo(file)) continue;
149
+ if (r.re.test(line)) {
150
+ findings.push({
151
+ severity: r.severity,
152
+ file,
153
+ line: i + 1,
154
+ rule: r.rule,
155
+ excerpt: line.trim().slice(0, 160),
156
+ });
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ function main() {
163
+ const files = listFiles();
164
+ if (files.length === 0) {
165
+ console.log('Final check: no source files in current diff to scan.');
166
+ process.exit(0);
167
+ }
168
+
169
+ for (const f of files) scan(f);
170
+
171
+ findings.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
172
+
173
+ const blocking = findings.filter(f => f.severity === 'CRITICAL' || f.severity === 'HIGH');
174
+ const warnings = findings.filter(f => f.severity === 'MEDIUM' || f.severity === 'LOW');
175
+
176
+ if (findings.length === 0) {
177
+ console.log(`Final check passed (${files.length} file${files.length === 1 ? '' : 's'} scanned, stack=${stackId}).`);
178
+ process.exit(0);
179
+ }
180
+
181
+ console.log(`Final check report — stack=${stackId}, files=${files.length}\n`);
182
+
183
+ if (blocking.length > 0) {
184
+ console.log(`BLOCKING — ${blocking.length} critical/high finding${blocking.length === 1 ? '' : 's'}:\n`);
185
+ for (const f of blocking) {
186
+ console.log(` [${f.severity}] ${f.file}:${f.line}`);
187
+ console.log(` ${f.rule}`);
188
+ console.log(` > ${f.excerpt}`);
189
+ }
190
+ console.log('');
191
+ }
192
+
193
+ if (warnings.length > 0) {
194
+ console.log(`WARNINGS — ${warnings.length} medium/low finding${warnings.length === 1 ? '' : 's'}:\n`);
195
+ for (const f of warnings.slice(0, 20)) {
196
+ console.log(` [${f.severity}] ${f.file}:${f.line} ${f.rule}`);
197
+ }
198
+ if (warnings.length > 20) console.log(` ... and ${warnings.length - 20} more`);
199
+ console.log('');
200
+ }
201
+
202
+ process.exit(blocking.length > 0 ? 1 : 0);
203
+ }
204
+
205
+ main();
@@ -9,6 +9,7 @@
9
9
  * 3. CLAUDE.md not updated
10
10
  * 4. CLAUDE.md missing required sections
11
11
  * 5. CLAUDE.md exceeds 40k chars
12
+ * 6. Secret pattern detected in committed/staged files (uses gitleaks if present, fallback to regex)
12
13
  */
13
14
 
14
15
  import { execSync } from 'child_process';
@@ -65,6 +66,71 @@ function getModifiedFiles(): string[] {
65
66
  return [...new Set([...staged, ...unstaged, ...untracked])];
66
67
  }
67
68
 
69
+ /**
70
+ * Run a command capturing stdout, stderr and exit code without throwing.
71
+ */
72
+ function runCapture(command: string): { code: number; stdout: string; stderr: string } {
73
+ try {
74
+ const stdout = execSync(command, {
75
+ cwd: PROJECT_DIR,
76
+ encoding: 'utf8',
77
+ stdio: ['pipe', 'pipe', 'pipe'],
78
+ });
79
+ return { code: 0, stdout: stdout.toString(), stderr: '' };
80
+ } catch (err: any) {
81
+ return {
82
+ code: typeof err?.status === 'number' ? err.status : 1,
83
+ stdout: err?.stdout?.toString?.() ?? '',
84
+ stderr: err?.stderr?.toString?.() ?? '',
85
+ };
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Scan staged + unstaged diff for secret patterns.
91
+ * Uses gitleaks when installed; otherwise applies a high-signal regex sweep.
92
+ * Returns a list of findings (empty = clean).
93
+ */
94
+ function scanSecrets(): string[] {
95
+ const findings: string[] = [];
96
+
97
+ const hasGitleaks = cmd('command -v gitleaks').length > 0;
98
+ if (hasGitleaks) {
99
+ const { code, stdout, stderr } = runCapture(
100
+ 'gitleaks detect --no-banner --redact --exit-code 1 --log-level=error'
101
+ );
102
+ if (code !== 0) {
103
+ const out = stdout + '\n' + stderr;
104
+ const lines = out
105
+ .split('\n')
106
+ .filter(l => l.includes('Finding:') || l.includes('File:') || l.includes('Secret:'));
107
+ if (lines.length > 0) findings.push(...lines.slice(0, 20));
108
+ else findings.push('gitleaks reported leaks (run `gitleaks detect --redact -v` for details)');
109
+ }
110
+ return findings;
111
+ }
112
+
113
+ // Fallback: high-signal regex sweep over staged + unstaged diff
114
+ const SECRET_RE = /(?:api[_-]?key|secret|token|bearer|password|aws_(?:access|secret)_key|private_key)\s*[:=]\s*["'][A-Za-z0-9/+=_\-.]{16,}["']/i;
115
+ const PUBLIC_LEAK_RE = /(?:NEXT_PUBLIC|VITE|REACT_APP)_[A-Z_]*(?:SECRET|TOKEN|PRIVATE|PASSWORD|CREDENTIAL)/;
116
+ const PLACEHOLDER_RE = /<\s*your[_\- ]|YOUR_[A-Z_]+|placeholder|example\.com|sk_test_|sk_xxx|xxxxxxxx/i;
117
+
118
+ const diff = cmd('git diff --cached -U0') + '\n' + cmd('git diff -U0');
119
+ if (!diff.trim()) return findings;
120
+
121
+ const lines = diff.split('\n');
122
+ for (const line of lines) {
123
+ if (!line.startsWith('+') || line.startsWith('+++')) continue;
124
+ if (PLACEHOLDER_RE.test(line)) continue;
125
+ if (SECRET_RE.test(line) || PUBLIC_LEAK_RE.test(line)) {
126
+ const masked = line.replace(/(["']).{8,}\1/, '$1[REDACTED]$1').slice(0, 160);
127
+ findings.push(masked);
128
+ if (findings.length >= 5) break;
129
+ }
130
+ }
131
+ return findings;
132
+ }
133
+
68
134
  function validate(): HookResult {
69
135
  const branch = getBranch();
70
136
  const isMain = branch === 'main' || branch === 'master';
@@ -132,11 +198,21 @@ function validate(): HookResult {
132
198
  };
133
199
  }
134
200
 
201
+ // 5. Secret scan (gitleaks or regex fallback)
202
+ const secrets = scanSecrets();
203
+ if (secrets.length > 0) {
204
+ return {
205
+ continue: true,
206
+ decision: 'block',
207
+ reason: `BLOCKED: Potential secrets detected in diff:\n${secrets.map(s => ` - ${s}`).join('\n')}\n\nRotate the credential, remove from diff, and re-run.`,
208
+ };
209
+ }
210
+
135
211
  // All good
136
212
  return {
137
213
  continue: false,
138
214
  decision: 'approve',
139
- reason: `ALL CHECKS PASSED ✅\nStack: ${stackId}\nBranch: ${branch}\nTree: Clean`,
215
+ reason: `ALL CHECKS PASSED ✅\nStack: ${stackId}\nBranch: ${branch}\nTree: Clean\nSecrets: clean`,
140
216
  };
141
217
  }
142
218