claudex-setup 1.14.1 → 1.15.1
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/bin/cli.js +18 -1
- package/content/case-study-template.md +1 -1
- package/content/claude-native-integration.md +4 -8
- package/package.json +1 -1
- package/src/activity.js +16 -1
- package/src/analyze.js +6 -2
- package/src/audit.js +7 -4
- package/src/benchmark.js +6 -1
- package/src/context.js +4 -0
- package/src/domain-packs.js +20 -12
- package/src/governance.js +19 -7
- package/src/mcp-packs.js +29 -24
- package/src/plans.js +4 -4
- package/src/setup.js +57 -59
- package/src/techniques.js +65 -51
package/bin/cli.js
CHANGED
|
@@ -249,6 +249,10 @@ async function main() {
|
|
|
249
249
|
process.exit(1);
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
if (options.require && normalizedCommand !== 'audit' && !['audit', 'discover'].includes(command)) {
|
|
253
|
+
console.error(`\n Warning: --require is only supported with the audit command. Ignoring for '${normalizedCommand}'.\n`);
|
|
254
|
+
}
|
|
255
|
+
|
|
252
256
|
if (!KNOWN_COMMANDS.includes(normalizedCommand)) {
|
|
253
257
|
const suggestion = suggestCommand(command);
|
|
254
258
|
console.error(`\n Error: Unknown command '${command}'.`);
|
|
@@ -326,7 +330,7 @@ async function main() {
|
|
|
326
330
|
} else if (normalizedCommand === 'insights') {
|
|
327
331
|
const https = require('https');
|
|
328
332
|
const url = 'https://claudex-insights.claudex.workers.dev/v1/stats';
|
|
329
|
-
https.get(url, (res) => {
|
|
333
|
+
const req = https.get(url, (res) => {
|
|
330
334
|
let data = '';
|
|
331
335
|
res.on('data', chunk => data += chunk);
|
|
332
336
|
res.on('end', () => {
|
|
@@ -357,6 +361,10 @@ async function main() {
|
|
|
357
361
|
}).on('error', () => {
|
|
358
362
|
console.log(' Could not reach insights server. Run locally: npx claudex-setup');
|
|
359
363
|
});
|
|
364
|
+
req.setTimeout(10000, () => {
|
|
365
|
+
req.destroy();
|
|
366
|
+
console.log(' Insights request timed out. Run locally: npx claudex-setup');
|
|
367
|
+
});
|
|
360
368
|
return; // keep process alive for http
|
|
361
369
|
} else if (normalizedCommand === 'augment' || normalizedCommand === 'suggest-only') {
|
|
362
370
|
const report = await analyzeProject({ ...options, mode: normalizedCommand });
|
|
@@ -445,6 +453,15 @@ async function main() {
|
|
|
445
453
|
await watch(options);
|
|
446
454
|
} else if (normalizedCommand === 'setup') {
|
|
447
455
|
await setup(options);
|
|
456
|
+
if (options.snapshot) {
|
|
457
|
+
const postSetupResult = await audit({ dir: options.dir, silent: true });
|
|
458
|
+
const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
|
|
459
|
+
sourceCommand: 'setup',
|
|
460
|
+
});
|
|
461
|
+
if (!options.json) {
|
|
462
|
+
console.log(` Snapshot saved: ${snapshot.relativePath}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
448
465
|
} else {
|
|
449
466
|
const result = await audit(options);
|
|
450
467
|
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
|
|
@@ -64,7 +64,7 @@ Key observations:
|
|
|
64
64
|
| Metric | Before | After | Change |
|
|
65
65
|
|--------|--------|-------|--------|
|
|
66
66
|
| Audit score | X | X | +X |
|
|
67
|
-
| Checks passing | X/
|
|
67
|
+
| Checks passing | X/84 | X/84 | +X |
|
|
68
68
|
| Time to first productive session | Xm | Xm | -Xm |
|
|
69
69
|
| [Other metric] | | | |
|
|
70
70
|
|
|
@@ -28,14 +28,10 @@ Add to `.claude/settings.json`:
|
|
|
28
28
|
"hooks": {
|
|
29
29
|
"SessionStart": [
|
|
30
30
|
{
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"timeout": 20,
|
|
36
|
-
"statusMessage": "Checking Claude Code setup..."
|
|
37
|
-
}
|
|
38
|
-
]
|
|
31
|
+
"type": "command",
|
|
32
|
+
"command": "node -e \"try{const r=require('child_process').execSync('npx claudex-setup --json 2>/dev/null',{timeout:15000}).toString();const d=JSON.parse(r);if(d.score<50)console.log(JSON.stringify({systemMessage:'⚠️ Claude Code setup score: '+d.score+'/100. Consider running: npx claudex-setup --lite'}))}catch(e){console.log('{}')}\"",
|
|
33
|
+
"timeout": 20,
|
|
34
|
+
"statusMessage": "Checking Claude Code setup..."
|
|
39
35
|
}
|
|
40
36
|
]
|
|
41
37
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudex-setup",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.1",
|
|
4
4
|
"description": "Score your repo's Claude Code setup against 84 checks. See gaps, apply fixes selectively with rollback, govern hooks and permissions, and benchmark impact — without breaking existing config.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/activity.js
CHANGED
|
@@ -2,8 +2,18 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { version } = require('../package.json');
|
|
4
4
|
|
|
5
|
+
let _lastTimestamp = '';
|
|
6
|
+
let _counter = 0;
|
|
7
|
+
|
|
5
8
|
function timestampId() {
|
|
6
|
-
|
|
9
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
10
|
+
if (ts === _lastTimestamp) {
|
|
11
|
+
_counter++;
|
|
12
|
+
return `${ts}-${_counter}`;
|
|
13
|
+
}
|
|
14
|
+
_lastTimestamp = ts;
|
|
15
|
+
_counter = 0;
|
|
16
|
+
return ts;
|
|
7
17
|
}
|
|
8
18
|
|
|
9
19
|
function ensureArtifactDirs(dir) {
|
|
@@ -122,6 +132,11 @@ function updateSnapshotIndex(snapshotDir, record) {
|
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
entries.push(record);
|
|
135
|
+
// Prune to keep last 200 entries
|
|
136
|
+
const MAX_INDEX_ENTRIES = 200;
|
|
137
|
+
if (entries.length > MAX_INDEX_ENTRIES) {
|
|
138
|
+
entries = entries.slice(entries.length - MAX_INDEX_ENTRIES);
|
|
139
|
+
}
|
|
125
140
|
fs.writeFileSync(indexPath, JSON.stringify(entries, null, 2), 'utf8');
|
|
126
141
|
}
|
|
127
142
|
|
package/src/analyze.js
CHANGED
|
@@ -424,7 +424,9 @@ function printAnalysis(report, options = {}) {
|
|
|
424
424
|
if (item.risk || item.confidence) {
|
|
425
425
|
console.log(c(` Risk: ${item.risk || 'low'} | Confidence: ${item.confidence || 'medium'}`, 'dim'));
|
|
426
426
|
}
|
|
427
|
-
|
|
427
|
+
if (item.fix && item.fix !== item.why) {
|
|
428
|
+
console.log(c(` Fix: ${item.fix}`, 'dim'));
|
|
429
|
+
}
|
|
428
430
|
});
|
|
429
431
|
console.log('');
|
|
430
432
|
}
|
|
@@ -515,7 +517,9 @@ function exportMarkdown(report) {
|
|
|
515
517
|
if (item.risk || item.confidence) {
|
|
516
518
|
lines.push(` - Risk / Confidence: ${item.risk || 'low'} / ${item.confidence || 'medium'}`);
|
|
517
519
|
}
|
|
518
|
-
|
|
520
|
+
if (item.fix && item.fix !== item.why) {
|
|
521
|
+
lines.push(` - Fix: ${item.fix}`);
|
|
522
|
+
}
|
|
519
523
|
});
|
|
520
524
|
lines.push('');
|
|
521
525
|
}
|
package/src/audit.js
CHANGED
|
@@ -85,12 +85,15 @@ function getPrioritizedFailed(failed) {
|
|
|
85
85
|
function getQuickWins(failed) {
|
|
86
86
|
const pool = getPrioritizedFailed(failed);
|
|
87
87
|
|
|
88
|
+
// QuickWins prioritize short fixes (easy to implement) first, then impact
|
|
88
89
|
return [...pool]
|
|
89
90
|
.sort((a, b) => {
|
|
91
|
+
const fixLenA = (a.fix || '').length;
|
|
92
|
+
const fixLenB = (b.fix || '').length;
|
|
93
|
+
if (fixLenA !== fixLenB) return fixLenA - fixLenB;
|
|
90
94
|
const impactA = IMPACT_ORDER[a.impact] ?? 0;
|
|
91
95
|
const impactB = IMPACT_ORDER[b.impact] ?? 0;
|
|
92
|
-
|
|
93
|
-
return (a.fix || '').length - (b.fix || '').length;
|
|
96
|
+
return impactB - impactA;
|
|
94
97
|
})
|
|
95
98
|
.slice(0, 3);
|
|
96
99
|
}
|
|
@@ -202,14 +205,14 @@ async function audit(options) {
|
|
|
202
205
|
const medium = failed.filter(r => r.impact === 'medium');
|
|
203
206
|
|
|
204
207
|
// Calculate score only from applicable checks
|
|
205
|
-
const weights = { critical: 15, high: 10, medium: 5 };
|
|
208
|
+
const weights = { critical: 15, high: 10, medium: 5, low: 2 };
|
|
206
209
|
const maxScore = applicable.reduce((sum, r) => sum + (weights[r.impact] || 5), 0);
|
|
207
210
|
const earnedScore = passed.reduce((sum, r) => sum + (weights[r.impact] || 5), 0);
|
|
208
211
|
const score = maxScore > 0 ? Math.round((earnedScore / maxScore) * 100) : 0;
|
|
209
212
|
|
|
210
213
|
// Detect scaffolded vs organic: if CLAUDE.md contains our version stamp, some checks
|
|
211
214
|
// are passing because WE generated them, not the user
|
|
212
|
-
const claudeMd = ctx.
|
|
215
|
+
const claudeMd = ctx.claudeMdContent() || '';
|
|
213
216
|
const isScaffolded = claudeMd.includes('Generated by claudex-setup') ||
|
|
214
217
|
claudeMd.includes('claudex-setup');
|
|
215
218
|
// Scaffolded checks: things our setup creates (CLAUDE.md, hooks, commands, agents, rules, skills)
|
package/src/benchmark.js
CHANGED
|
@@ -21,6 +21,9 @@ function copyProject(sourceDir, targetDir) {
|
|
|
21
21
|
copyProject(from, to);
|
|
22
22
|
} else if (entry.isFile()) {
|
|
23
23
|
fs.copyFileSync(from, to);
|
|
24
|
+
} else if (entry.isSymbolicLink && entry.isSymbolicLink()) {
|
|
25
|
+
// Symlinks are skipped in benchmark sandbox — log for awareness
|
|
26
|
+
process.stderr.write(` Note: symlink skipped in benchmark: ${entry.name}\n`);
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
29
|
}
|
|
@@ -89,7 +92,9 @@ function buildExecutiveSummary(before, after, workflowEvidence) {
|
|
|
89
92
|
const workflowCoverage = workflowEvidence.summary.coverageScore;
|
|
90
93
|
let headline = 'Benchmark did not improve the score in this run.';
|
|
91
94
|
|
|
92
|
-
if (scoreDelta
|
|
95
|
+
if (scoreDelta < 0) {
|
|
96
|
+
headline = `Warning: score decreased by ${Math.abs(scoreDelta)} points. Setup may have introduced a regression.`;
|
|
97
|
+
} else if (scoreDelta > 0) {
|
|
93
98
|
headline = `Benchmark improved readiness by ${scoreDelta} points without touching the original repo.`;
|
|
94
99
|
} else if (before.score >= 85 && after.score >= before.score && workflowCoverage >= 80) {
|
|
95
100
|
headline = 'Benchmark confirmed the repo already meets the starter-safe baseline without regression.';
|
package/src/context.js
CHANGED
|
@@ -67,6 +67,10 @@ class ProjectContext {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
claudeMdContent() {
|
|
71
|
+
return this.fileContent('CLAUDE.md') || this.fileContent('.claude/CLAUDE.md');
|
|
72
|
+
}
|
|
73
|
+
|
|
70
74
|
fileContent(filePath) {
|
|
71
75
|
if (this._cache[filePath] !== undefined) return this._cache[filePath];
|
|
72
76
|
const fullPath = path.join(this.dir, filePath);
|
package/src/domain-packs.js
CHANGED
|
@@ -156,17 +156,18 @@ function detectDomainPacks(ctx, stacks, assets = null) {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
const hasFrontend = stackKeys.has('react') || stackKeys.has('nextjs') || stackKeys.has('vue') ||
|
|
159
|
-
stackKeys.has('angular') || stackKeys.has('svelte') || ctx.hasDir('components') || ctx.hasDir('
|
|
159
|
+
stackKeys.has('angular') || stackKeys.has('svelte') || ctx.hasDir('components') || ctx.hasDir('pages') ||
|
|
160
|
+
(ctx.hasDir('app') && (deps.next || deps.react || deps.vue || deps['@angular/core'] || deps.svelte));
|
|
160
161
|
const hasBackend = stackKeys.has('node') || stackKeys.has('python') || stackKeys.has('django') ||
|
|
161
162
|
stackKeys.has('fastapi') || stackKeys.has('go') || stackKeys.has('rust') || stackKeys.has('java') ||
|
|
162
163
|
ctx.hasDir('api') || ctx.hasDir('routes') || ctx.hasDir('services') || ctx.hasDir('controllers');
|
|
163
164
|
const hasData = ctx.hasDir('dags') || ctx.hasDir('jobs') || ctx.hasDir('workers') ||
|
|
164
|
-
ctx.hasDir('
|
|
165
|
+
ctx.hasDir('migrations') || ctx.hasDir('db') ||
|
|
165
166
|
deps.dbt || deps['apache-airflow'] || deps.pandas || deps.polars || deps.duckdb;
|
|
166
167
|
const hasInfra = stackKeys.has('docker') || stackKeys.has('terraform') || stackKeys.has('kubernetes') ||
|
|
167
168
|
ctx.files.includes('wrangler.toml') || ctx.files.includes('serverless.yml') || ctx.files.includes('serverless.yaml') ||
|
|
168
169
|
ctx.files.includes('cdk.json') || ctx.hasDir('infra') || ctx.hasDir('deploy') || ctx.hasDir('helm');
|
|
169
|
-
const isOss = !!ctx.fileContent('LICENSE') &&
|
|
170
|
+
const isOss = !!ctx.fileContent('LICENSE') && pkg.private !== true;
|
|
170
171
|
const isEnterpriseGoverned = !!(assets && assets.permissions && assets.permissions.hasDenyRules) &&
|
|
171
172
|
!!(assets && assets.files && assets.files.settings) && ctx.hasDir('.github/workflows');
|
|
172
173
|
|
|
@@ -218,7 +219,8 @@ function detectDomainPacks(ctx, stacks, assets = null) {
|
|
|
218
219
|
|
|
219
220
|
// Monorepo detection
|
|
220
221
|
const isMonorepo = ctx.files.includes('nx.json') || ctx.files.includes('turbo.json') ||
|
|
221
|
-
ctx.files.includes('lerna.json') || ctx.
|
|
222
|
+
ctx.files.includes('lerna.json') || ctx.files.includes('pnpm-workspace.yaml') ||
|
|
223
|
+
ctx.hasDir('packages') ||
|
|
222
224
|
(pkg.workspaces && (Array.isArray(pkg.workspaces) ? pkg.workspaces.length > 0 : true));
|
|
223
225
|
if (isMonorepo) {
|
|
224
226
|
addMatch('monorepo', [
|
|
@@ -259,7 +261,8 @@ function detectDomainPacks(ctx, stacks, assets = null) {
|
|
|
259
261
|
// E-commerce detection
|
|
260
262
|
const isEcommerce = deps.stripe || deps['@stripe/stripe-js'] || deps.shopify || deps['@shopify/shopify-api'] ||
|
|
261
263
|
deps.woocommerce || deps.paypal || deps['@paypal/react-paypal-js'] || deps.square || deps['@adyen/adyen-web'] ||
|
|
262
|
-
deps.medusa || deps.saleor ||
|
|
264
|
+
deps.medusa || deps.saleor || deps.braintree || deps['@mollie/api-client'] ||
|
|
265
|
+
deps.razorpay || deps['@paddle/paddle-node-sdk'] || deps['@lemonsqueezy/lemonsqueezy.js'] ||
|
|
263
266
|
ctx.hasDir('products') || ctx.hasDir('checkout') || ctx.hasDir('cart');
|
|
264
267
|
if (isEcommerce) {
|
|
265
268
|
addMatch('ecommerce', [
|
|
@@ -274,8 +277,10 @@ function detectDomainPacks(ctx, stacks, assets = null) {
|
|
|
274
277
|
deps['@anthropic-ai/sdk'] || deps.transformers || deps.torch || deps.tensorflow ||
|
|
275
278
|
deps.llamaindex || deps['llama-index'] || deps.crewai || deps.autogen ||
|
|
276
279
|
deps['@ai-sdk/core'] || deps.ollama ||
|
|
277
|
-
|
|
278
|
-
|
|
280
|
+
deps['@microsoft/semantic-kernel'] || deps['haystack-ai'] || deps['dspy-ai'] ||
|
|
281
|
+
deps.instructor || deps['@google/generative-ai'] || deps.cohere || deps.mistralai ||
|
|
282
|
+
ctx.hasDir('chains') || ctx.hasDir('agents') || ctx.hasDir('prompts');
|
|
283
|
+
if (isAiMl) {
|
|
279
284
|
addMatch('ai-ml', [
|
|
280
285
|
'Detected AI/ML dependencies or agent structure.',
|
|
281
286
|
deps.langchain ? 'LangChain detected.' : null,
|
|
@@ -287,7 +292,7 @@ function detectDomainPacks(ctx, stacks, assets = null) {
|
|
|
287
292
|
const isDevopsCicd = ctx.hasDir('.github/workflows') || ctx.hasDir('.circleci') ||
|
|
288
293
|
ctx.files.includes('Jenkinsfile') || ctx.files.includes('.gitlab-ci.yml') ||
|
|
289
294
|
ctx.hasDir('deploy') || ctx.hasDir('scripts/deploy');
|
|
290
|
-
if (isDevopsCicd
|
|
295
|
+
if (isDevopsCicd) {
|
|
291
296
|
addMatch('devops-cicd', [
|
|
292
297
|
'Detected CI/CD pipelines or deployment scripts.',
|
|
293
298
|
ctx.hasDir('.github/workflows') ? 'GitHub Actions detected.' : null,
|
|
@@ -298,8 +303,10 @@ function detectDomainPacks(ctx, stacks, assets = null) {
|
|
|
298
303
|
// Design system detection
|
|
299
304
|
const isDesignSystem = deps.storybook || deps['@storybook/react'] || deps['@storybook/vue3'] ||
|
|
300
305
|
deps.chromatic || deps['style-dictionary'] || deps['@tokens-studio/sd-transforms'] ||
|
|
301
|
-
|
|
302
|
-
ctx.hasDir('
|
|
306
|
+
deps['@radix-ui/react-primitives'] || deps['@headlessui/react'] ||
|
|
307
|
+
ctx.hasDir('tokens') || ctx.hasDir('design-tokens') || ctx.hasDir('primitives') ||
|
|
308
|
+
ctx.hasDir('.storybook') ||
|
|
309
|
+
(ctx.hasDir('components') && (deps['tailwindcss'] || deps.tailwindcss) && ctx.hasDir('packages'));
|
|
303
310
|
if (isDesignSystem) {
|
|
304
311
|
addMatch('design-system', [
|
|
305
312
|
'Detected design system or component library signals.',
|
|
@@ -321,8 +328,9 @@ function detectDomainPacks(ctx, stacks, assets = null) {
|
|
|
321
328
|
}
|
|
322
329
|
|
|
323
330
|
// Security-focused detection
|
|
324
|
-
const
|
|
325
|
-
|
|
331
|
+
const hasAuthDeps = deps.bcrypt || deps.jsonwebtoken || deps.passport || deps['next-auth'] ||
|
|
332
|
+
deps['@auth/core'] || deps['lucia'] || deps['better-auth'];
|
|
333
|
+
const isSecurityFocused = (hasSecurityPolicy || hasAuthDeps) && hasBackend;
|
|
326
334
|
if (isSecurityFocused && !isRegulated) {
|
|
327
335
|
addMatch('security-focused', [
|
|
328
336
|
'Detected security-sensitive backend with auth dependencies.',
|
package/src/governance.js
CHANGED
|
@@ -124,7 +124,7 @@ const HOOK_REGISTRY = [
|
|
|
124
124
|
},
|
|
125
125
|
{
|
|
126
126
|
key: 'session-init',
|
|
127
|
-
file: '.claude/hooks/
|
|
127
|
+
file: '.claude/hooks/session-start.sh',
|
|
128
128
|
triggerPoint: 'SessionStart',
|
|
129
129
|
matcher: null,
|
|
130
130
|
purpose: 'Rotates large log files and loads workspace context at session start.',
|
|
@@ -254,36 +254,46 @@ function buildHookConfig(hookFiles, profileKey) {
|
|
|
254
254
|
return {};
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
// Detect hook runtime: .js files use node, .sh files use bash
|
|
258
|
+
const hookCommand = (file) => {
|
|
259
|
+
if (file.endsWith('.js')) return `node .claude/hooks/${file}`;
|
|
260
|
+
return `bash .claude/hooks/${file}`;
|
|
261
|
+
};
|
|
262
|
+
const isSecrets = (f) => f === 'protect-secrets.sh' || f === 'protect-secrets.js';
|
|
263
|
+
const isSession = (f) => f === 'session-start.sh' || f === 'session-start.js';
|
|
264
|
+
|
|
257
265
|
const hookConfig = {
|
|
258
266
|
PostToolUse: [{
|
|
259
267
|
matcher: 'Write|Edit',
|
|
260
268
|
hooks: uniqueFiles
|
|
261
|
-
.filter(file => file
|
|
269
|
+
.filter(file => !isSecrets(file) && !isSession(file))
|
|
262
270
|
.map(file => ({
|
|
263
271
|
type: 'command',
|
|
264
|
-
command:
|
|
272
|
+
command: hookCommand(file),
|
|
265
273
|
timeout: 10,
|
|
266
274
|
})),
|
|
267
275
|
}],
|
|
268
276
|
};
|
|
269
277
|
|
|
270
|
-
|
|
278
|
+
const secretsFile = uniqueFiles.find(isSecrets);
|
|
279
|
+
if (secretsFile) {
|
|
271
280
|
hookConfig.PreToolUse = [{
|
|
272
281
|
matcher: 'Read|Write|Edit',
|
|
273
282
|
hooks: [{
|
|
274
283
|
type: 'command',
|
|
275
|
-
command:
|
|
284
|
+
command: hookCommand(secretsFile),
|
|
276
285
|
timeout: 5,
|
|
277
286
|
}],
|
|
278
287
|
}];
|
|
279
288
|
}
|
|
280
289
|
|
|
281
|
-
|
|
290
|
+
const sessionFile = uniqueFiles.find(isSession);
|
|
291
|
+
if (sessionFile) {
|
|
282
292
|
hookConfig.SessionStart = [{
|
|
283
293
|
matcher: '*',
|
|
284
294
|
hooks: [{
|
|
285
295
|
type: 'command',
|
|
286
|
-
command:
|
|
296
|
+
command: hookCommand(sessionFile),
|
|
287
297
|
timeout: 5,
|
|
288
298
|
}],
|
|
289
299
|
}];
|
|
@@ -451,6 +461,8 @@ function renderGovernanceMarkdown(summary) {
|
|
|
451
461
|
}
|
|
452
462
|
|
|
453
463
|
lines.push('');
|
|
464
|
+
lines.push('---');
|
|
465
|
+
lines.push(`*Generated by claudex-setup v${require('../package.json').version} on ${new Date().toISOString().split('T')[0]}*`);
|
|
454
466
|
return lines.join('\n');
|
|
455
467
|
}
|
|
456
468
|
|
package/src/mcp-packs.js
CHANGED
|
@@ -40,12 +40,11 @@ const MCP_PACKS = [
|
|
|
40
40
|
key: 'postgres-mcp',
|
|
41
41
|
label: 'PostgreSQL',
|
|
42
42
|
useWhen: 'Repos with PostgreSQL databases that benefit from schema inspection and query assistance.',
|
|
43
|
-
adoption: 'Useful for backend-api and data-pipeline repos.
|
|
43
|
+
adoption: 'Useful for backend-api and data-pipeline repos. Pass connection string as CLI argument.',
|
|
44
44
|
servers: {
|
|
45
45
|
postgres: {
|
|
46
46
|
command: 'npx',
|
|
47
|
-
args: ['-y', '@modelcontextprotocol/server-postgres'],
|
|
48
|
-
env: { DATABASE_URL: '${DATABASE_URL}' },
|
|
47
|
+
args: ['-y', '@modelcontextprotocol/server-postgres', '${DATABASE_URL}'],
|
|
49
48
|
},
|
|
50
49
|
},
|
|
51
50
|
},
|
|
@@ -77,7 +76,7 @@ const MCP_PACKS = [
|
|
|
77
76
|
key: 'docker-mcp',
|
|
78
77
|
label: 'Docker',
|
|
79
78
|
useWhen: 'Repos with containerized workflows that benefit from container management during Claude sessions.',
|
|
80
|
-
adoption: '
|
|
79
|
+
adoption: 'Community Docker MCP server. Requires Docker running locally. Note: community-maintained package.',
|
|
81
80
|
servers: {
|
|
82
81
|
docker: {
|
|
83
82
|
command: 'npx',
|
|
@@ -128,7 +127,7 @@ const MCP_PACKS = [
|
|
|
128
127
|
key: 'slack-mcp',
|
|
129
128
|
label: 'Slack',
|
|
130
129
|
useWhen: 'Teams using Slack that want Claude to draft, preview, or post messages.',
|
|
131
|
-
adoption: '
|
|
130
|
+
adoption: 'Community Slack MCP server (supports OAuth and stealth mode). Requires SLACK_BOT_TOKEN or SLACK_COOKIES env var.',
|
|
132
131
|
servers: {
|
|
133
132
|
slack: {
|
|
134
133
|
command: 'npx',
|
|
@@ -154,7 +153,7 @@ const MCP_PACKS = [
|
|
|
154
153
|
key: 'figma-mcp',
|
|
155
154
|
label: 'Figma',
|
|
156
155
|
useWhen: 'Design-heavy repos where Claude needs access to Figma designs and components.',
|
|
157
|
-
adoption: '
|
|
156
|
+
adoption: 'Community Figma MCP server. Requires FIGMA_ACCESS_TOKEN env var. Note: community-maintained package.',
|
|
158
157
|
servers: {
|
|
159
158
|
figma: {
|
|
160
159
|
command: 'npx',
|
|
@@ -202,8 +201,8 @@ const MCP_PACKS = [
|
|
|
202
201
|
},
|
|
203
202
|
{
|
|
204
203
|
key: 'jira-confluence',
|
|
205
|
-
label: 'Jira
|
|
206
|
-
useWhen: 'Teams using Atlassian for issue tracking and
|
|
204
|
+
label: 'Jira',
|
|
205
|
+
useWhen: 'Teams using Atlassian Jira for issue tracking and project management.',
|
|
207
206
|
adoption: 'Requires ATLASSIAN_API_TOKEN and ATLASSIAN_EMAIL env vars.',
|
|
208
207
|
servers: {
|
|
209
208
|
jira: {
|
|
@@ -217,12 +216,12 @@ const MCP_PACKS = [
|
|
|
217
216
|
key: 'ga4-analytics',
|
|
218
217
|
label: 'Google Analytics 4',
|
|
219
218
|
useWhen: 'Repos with web analytics needs — live GA4 data, attribution, and audience insights.',
|
|
220
|
-
adoption: 'Requires
|
|
219
|
+
adoption: 'Requires GA4_PROPERTY_ID and either GOOGLE_APPLICATION_CREDENTIALS or ADC for auth.',
|
|
221
220
|
servers: {
|
|
222
221
|
ga4: {
|
|
223
222
|
command: 'npx',
|
|
224
223
|
args: ['-y', 'mcp-server-ga4'],
|
|
225
|
-
env: { GA4_PROPERTY_ID: '${GA4_PROPERTY_ID}' },
|
|
224
|
+
env: { GA4_PROPERTY_ID: '${GA4_PROPERTY_ID}', GOOGLE_APPLICATION_CREDENTIALS: '${GOOGLE_APPLICATION_CREDENTIALS}' },
|
|
226
225
|
},
|
|
227
226
|
},
|
|
228
227
|
},
|
|
@@ -230,12 +229,12 @@ const MCP_PACKS = [
|
|
|
230
229
|
key: 'search-console',
|
|
231
230
|
label: 'Google Search Console',
|
|
232
231
|
useWhen: 'SEO-focused repos that need search performance data, indexing status, and sitemap insights.',
|
|
233
|
-
adoption: 'Requires
|
|
232
|
+
adoption: 'Requires Google OAuth client credentials (client ID + secret). Uses OAuth consent flow.',
|
|
234
233
|
servers: {
|
|
235
234
|
gsc: {
|
|
236
235
|
command: 'npx',
|
|
237
236
|
args: ['-y', 'mcp-gsc@latest'],
|
|
238
|
-
env: {
|
|
237
|
+
env: { GOOGLE_CLIENT_ID: '${GOOGLE_CLIENT_ID}', GOOGLE_CLIENT_SECRET: '${GOOGLE_CLIENT_SECRET}' },
|
|
239
238
|
},
|
|
240
239
|
},
|
|
241
240
|
},
|
|
@@ -243,7 +242,7 @@ const MCP_PACKS = [
|
|
|
243
242
|
key: 'n8n-workflows',
|
|
244
243
|
label: 'n8n Workflow Automation',
|
|
245
244
|
useWhen: 'Teams using n8n for workflow automation with 1,396 integration nodes.',
|
|
246
|
-
adoption: 'Requires n8n instance URL
|
|
245
|
+
adoption: 'Requires n8n instance URL and API key.',
|
|
247
246
|
servers: {
|
|
248
247
|
n8n: {
|
|
249
248
|
command: 'npx',
|
|
@@ -256,7 +255,7 @@ const MCP_PACKS = [
|
|
|
256
255
|
key: 'zendesk-mcp',
|
|
257
256
|
label: 'Zendesk',
|
|
258
257
|
useWhen: 'Support teams using Zendesk for ticket management and help center content.',
|
|
259
|
-
adoption: 'Requires ZENDESK_API_TOKEN
|
|
258
|
+
adoption: 'Requires ZENDESK_API_TOKEN and ZENDESK_SUBDOMAIN env vars.',
|
|
260
259
|
servers: {
|
|
261
260
|
zendesk: {
|
|
262
261
|
command: 'npx',
|
|
@@ -269,7 +268,7 @@ const MCP_PACKS = [
|
|
|
269
268
|
key: 'infisical-secrets',
|
|
270
269
|
label: 'Infisical Secrets',
|
|
271
270
|
useWhen: 'Repos using Infisical for secrets management with auto-rotation.',
|
|
272
|
-
adoption: 'Requires INFISICAL_TOKEN env var.
|
|
271
|
+
adoption: 'Requires INFISICAL_TOKEN env var.',
|
|
273
272
|
servers: {
|
|
274
273
|
infisical: {
|
|
275
274
|
command: 'npx',
|
|
@@ -282,7 +281,7 @@ const MCP_PACKS = [
|
|
|
282
281
|
key: 'shopify-mcp',
|
|
283
282
|
label: 'Shopify',
|
|
284
283
|
useWhen: 'Shopify stores and apps that need API schema access and deployment tooling.',
|
|
285
|
-
adoption: '
|
|
284
|
+
adoption: 'Community Shopify MCP server for GraphQL API access. Requires SHOPIFY_ACCESS_TOKEN env var.',
|
|
286
285
|
servers: {
|
|
287
286
|
shopify: {
|
|
288
287
|
command: 'npx',
|
|
@@ -307,7 +306,7 @@ const MCP_PACKS = [
|
|
|
307
306
|
{
|
|
308
307
|
key: 'blender-mcp',
|
|
309
308
|
label: 'Blender 3D',
|
|
310
|
-
useWhen: '3D modeling, animation, or rendering repos that use Blender.
|
|
309
|
+
useWhen: '3D modeling, animation, or rendering repos that use Blender.',
|
|
311
310
|
adoption: 'Requires Blender installed locally. Python bridge.',
|
|
312
311
|
servers: {
|
|
313
312
|
blender: {
|
|
@@ -320,7 +319,7 @@ const MCP_PACKS = [
|
|
|
320
319
|
key: 'wordpress-mcp',
|
|
321
320
|
label: 'WordPress',
|
|
322
321
|
useWhen: 'WordPress sites needing content management, site ops, and plugin workflows.',
|
|
323
|
-
adoption: 'Requires WP_URL and WP_AUTH_TOKEN env vars.
|
|
322
|
+
adoption: 'Requires WP_URL and WP_AUTH_TOKEN env vars.',
|
|
324
323
|
servers: {
|
|
325
324
|
wordpress: {
|
|
326
325
|
command: 'npx',
|
|
@@ -534,14 +533,17 @@ function recommendMcpPacks(stacks = [], domainPacks = [], options = {}) {
|
|
|
534
533
|
recommended.add('jira-confluence');
|
|
535
534
|
}
|
|
536
535
|
|
|
537
|
-
// Analytics for ecommerce
|
|
538
|
-
if (domainKeys.has('ecommerce')
|
|
536
|
+
// Analytics for ecommerce (docs-content repos rarely need GA4/GSC)
|
|
537
|
+
if (domainKeys.has('ecommerce')) {
|
|
539
538
|
recommended.add('ga4-analytics');
|
|
540
539
|
recommended.add('search-console');
|
|
541
540
|
}
|
|
542
541
|
|
|
543
|
-
// Shopify
|
|
544
|
-
if (domainKeys.has('ecommerce')
|
|
542
|
+
// Shopify only when Shopify signals are present
|
|
543
|
+
if (domainKeys.has('ecommerce') && ctx && (
|
|
544
|
+
hasDependency(deps, 'shopify') || hasDependency(deps, '@shopify/shopify-api') ||
|
|
545
|
+
hasFileContentMatch(ctx, '.env', /shopify/i) || hasFileContentMatch(ctx, '.env.example', /shopify/i)
|
|
546
|
+
)) {
|
|
545
547
|
recommended.add('shopify-mcp');
|
|
546
548
|
}
|
|
547
549
|
|
|
@@ -550,8 +552,11 @@ function recommendMcpPacks(stacks = [], domainPacks = [], options = {}) {
|
|
|
550
552
|
recommended.add('huggingface-mcp');
|
|
551
553
|
}
|
|
552
554
|
|
|
553
|
-
// Zendesk
|
|
554
|
-
if (domainKeys.has('enterprise-governed')
|
|
555
|
+
// Zendesk only when Zendesk signals are present
|
|
556
|
+
if (domainKeys.has('enterprise-governed') && ctx && (
|
|
557
|
+
hasDependency(deps, 'zendesk') || hasDependency(deps, 'node-zendesk') ||
|
|
558
|
+
hasFileContentMatch(ctx, '.env', /zendesk/i) || hasFileContentMatch(ctx, '.env.example', /zendesk/i)
|
|
559
|
+
)) {
|
|
555
560
|
recommended.add('zendesk-mcp');
|
|
556
561
|
}
|
|
557
562
|
|
package/src/plans.js
CHANGED
|
@@ -63,7 +63,7 @@ function previewContent(content) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
function riskFromImpact(impact) {
|
|
66
|
-
if (impact === 'critical') return '
|
|
66
|
+
if (impact === 'critical') return 'high';
|
|
67
67
|
if (impact === 'high') return 'medium';
|
|
68
68
|
return 'low';
|
|
69
69
|
}
|
|
@@ -260,7 +260,7 @@ function buildAgentPatchFiles(ctx) {
|
|
|
260
260
|
|
|
261
261
|
function buildHookSettings(ctx, plannedHookFiles, options = {}) {
|
|
262
262
|
const existing = ctx.hasDir('.claude/hooks')
|
|
263
|
-
? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh'))
|
|
263
|
+
? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh') || file.endsWith('.js'))
|
|
264
264
|
: [];
|
|
265
265
|
const hookFiles = [...new Set([...existing, ...plannedHookFiles])].sort();
|
|
266
266
|
if (hookFiles.length === 0) {
|
|
@@ -385,7 +385,7 @@ async function buildProposalBundle(options) {
|
|
|
385
385
|
if (templateKey === 'hooks') {
|
|
386
386
|
const plannedHookFiles = templateFiles
|
|
387
387
|
.map(file => path.basename(file.path))
|
|
388
|
-
.filter(file => file.endsWith('.sh'));
|
|
388
|
+
.filter(file => file.endsWith('.sh') || file.endsWith('.js'));
|
|
389
389
|
const settingsFile = buildHookSettings(ctx, plannedHookFiles, options);
|
|
390
390
|
if (settingsFile) {
|
|
391
391
|
templateFiles.push(settingsFile);
|
|
@@ -476,7 +476,7 @@ function applyRuntimeSettingsOverlays(bundle, options) {
|
|
|
476
476
|
|
|
477
477
|
const ctx = new ProjectContext(options.dir);
|
|
478
478
|
const existingHooks = ctx.hasDir('.claude/hooks')
|
|
479
|
-
? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh'))
|
|
479
|
+
? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh') || file.endsWith('.js'))
|
|
480
480
|
: [];
|
|
481
481
|
|
|
482
482
|
const proposals = bundle.proposals.map((proposal) => {
|
package/src/setup.js
CHANGED
|
@@ -367,7 +367,7 @@ function generateMermaid(dirs, stacks) {
|
|
|
367
367
|
nodes.push(addNode('Entry Point', 'round'));
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
-
const root = ids['Next.js'] || ids['Django'] || ids['FastAPI'] || ids['Entry Point'];
|
|
370
|
+
const root = ids['Next.js'] || ids['Django'] || ids['FastAPI'] || ids['Entry Point'] || 'A';
|
|
371
371
|
const pickNodeId = (...labels) => labels.map(label => ids[label]).find(Boolean) || root;
|
|
372
372
|
|
|
373
373
|
// Detect layers
|
|
@@ -782,66 +782,64 @@ ${verificationSteps.join('\n')}
|
|
|
782
782
|
},
|
|
783
783
|
|
|
784
784
|
'hooks': () => ({
|
|
785
|
-
'on-edit-lint.
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
if
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
fi
|
|
785
|
+
'on-edit-lint.js': `#!/usr/bin/env node
|
|
786
|
+
// PostToolUse hook - runs linter after file edits
|
|
787
|
+
const { execSync } = require('child_process');
|
|
788
|
+
const fs = require('fs');
|
|
789
|
+
try {
|
|
790
|
+
if (fs.existsSync('package.json')) {
|
|
791
|
+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
792
|
+
if (pkg.scripts && pkg.scripts.lint) {
|
|
793
|
+
execSync('npm run lint --silent', { stdio: 'ignore', timeout: 30000 });
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
} catch (e) { /* linter not available or failed - non-blocking */ }
|
|
798
797
|
`,
|
|
799
|
-
'protect-secrets.
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
798
|
+
'protect-secrets.js': `#!/usr/bin/env node
|
|
799
|
+
// PreToolUse hook - blocks reads of secret files
|
|
800
|
+
let input = '';
|
|
801
|
+
process.stdin.on('data', d => input += d);
|
|
802
|
+
process.stdin.on('end', () => {
|
|
803
|
+
try {
|
|
804
|
+
const data = JSON.parse(input);
|
|
805
|
+
const fp = (data.tool_input && data.tool_input.file_path) || '';
|
|
806
|
+
if (/\\.env$|\\.env\\.|secrets[\\/\\\\]|credentials|\\.pem$|\\.key$/i.test(fp)) {
|
|
807
|
+
console.log(JSON.stringify({ decision: 'block', reason: 'Blocked: accessing secret/credential files is not allowed.' }));
|
|
808
|
+
} else {
|
|
809
|
+
console.log(JSON.stringify({ decision: 'allow' }));
|
|
810
|
+
}
|
|
811
|
+
} catch (e) {
|
|
812
|
+
console.log(JSON.stringify({ decision: 'allow' }));
|
|
813
|
+
}
|
|
814
|
+
});
|
|
809
815
|
`,
|
|
810
|
-
'log-changes.
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
|
|
829
|
-
echo "[$TIMESTAMP] $TOOL_NAME: $FILE_PATH" >> "$LOG_FILE"
|
|
830
|
-
|
|
831
|
-
exit 0
|
|
816
|
+
'log-changes.js': `#!/usr/bin/env node
|
|
817
|
+
// PostToolUse hook - logs all file changes with timestamps
|
|
818
|
+
const fs = require('fs');
|
|
819
|
+
const path = require('path');
|
|
820
|
+
let input = '';
|
|
821
|
+
process.stdin.on('data', d => input += d);
|
|
822
|
+
process.stdin.on('end', () => {
|
|
823
|
+
try {
|
|
824
|
+
const data = JSON.parse(input);
|
|
825
|
+
const fp = (data.tool_input && data.tool_input.file_path) || '';
|
|
826
|
+
if (!fp) process.exit(0);
|
|
827
|
+
const toolName = data.tool_name || 'unknown';
|
|
828
|
+
const logDir = path.join('.claude', 'logs');
|
|
829
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
830
|
+
const ts = new Date().toISOString().replace('T', ' ').split('.')[0];
|
|
831
|
+
fs.appendFileSync(path.join(logDir, 'file-changes.log'), \`[\${ts}] \${toolName}: \${fp}\\n\`);
|
|
832
|
+
} catch (e) { /* non-blocking */ }
|
|
833
|
+
});
|
|
832
834
|
`,
|
|
833
|
-
'session-start.
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
|
|
842
|
-
echo "[$TIMESTAMP] session started" >> "$LOG_FILE"
|
|
843
|
-
|
|
844
|
-
exit 0
|
|
835
|
+
'session-start.js': `#!/usr/bin/env node
|
|
836
|
+
// SessionStart hook - prepares logs and records session entry
|
|
837
|
+
const fs = require('fs');
|
|
838
|
+
const path = require('path');
|
|
839
|
+
const logDir = path.join('.claude', 'logs');
|
|
840
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
841
|
+
const ts = new Date().toISOString().replace('T', ' ').split('.')[0];
|
|
842
|
+
fs.appendFileSync(path.join(logDir, 'sessions.log'), \`[\${ts}] session started\\n\`);
|
|
845
843
|
`,
|
|
846
844
|
}),
|
|
847
845
|
|
|
@@ -1219,7 +1217,7 @@ async function setup(options) {
|
|
|
1219
1217
|
const hooksDir = path.join(options.dir, '.claude/hooks');
|
|
1220
1218
|
const settingsPath = path.join(options.dir, '.claude/settings.json');
|
|
1221
1219
|
if (fs.existsSync(hooksDir) && !fs.existsSync(settingsPath)) {
|
|
1222
|
-
const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh'));
|
|
1220
|
+
const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh') || f.endsWith('.js'));
|
|
1223
1221
|
if (hookFiles.length > 0) {
|
|
1224
1222
|
const settings = buildSettingsForProfile({
|
|
1225
1223
|
profileKey: options.profile || 'safe-write',
|
package/src/techniques.js
CHANGED
|
@@ -30,7 +30,7 @@ const TECHNIQUES = {
|
|
|
30
30
|
id: 51,
|
|
31
31
|
name: 'Mermaid architecture diagram',
|
|
32
32
|
check: (ctx) => {
|
|
33
|
-
const md = ctx.
|
|
33
|
+
const md = ctx.claudeMdContent() || '';
|
|
34
34
|
return md.includes('mermaid') || md.includes('graph ') || md.includes('flowchart ');
|
|
35
35
|
},
|
|
36
36
|
impact: 'high',
|
|
@@ -55,7 +55,7 @@ const TECHNIQUES = {
|
|
|
55
55
|
id: 763,
|
|
56
56
|
name: 'CLAUDE.md uses @import for modularity',
|
|
57
57
|
check: (ctx) => {
|
|
58
|
-
const md = ctx.
|
|
58
|
+
const md = ctx.claudeMdContent() || '';
|
|
59
59
|
return md.includes('@import');
|
|
60
60
|
},
|
|
61
61
|
impact: 'medium',
|
|
@@ -69,8 +69,8 @@ const TECHNIQUES = {
|
|
|
69
69
|
id: 681,
|
|
70
70
|
name: 'CLAUDE.md under 200 lines (concise)',
|
|
71
71
|
check: (ctx) => {
|
|
72
|
-
const md = ctx.
|
|
73
|
-
return md.split('\n').length
|
|
72
|
+
const md = ctx.claudeMdContent() || '';
|
|
73
|
+
return md.split('\n').length <= 200;
|
|
74
74
|
},
|
|
75
75
|
impact: 'medium',
|
|
76
76
|
rating: 4,
|
|
@@ -87,8 +87,9 @@ const TECHNIQUES = {
|
|
|
87
87
|
id: 93,
|
|
88
88
|
name: 'Verification criteria in CLAUDE.md',
|
|
89
89
|
check: (ctx) => {
|
|
90
|
-
const md = ctx.
|
|
91
|
-
return
|
|
90
|
+
const md = ctx.claudeMdContent() || '';
|
|
91
|
+
return /\b(npm test|yarn test|pnpm test|pytest|go test|make test|npm run lint|yarn lint|npx |ruff |eslint)\b/i.test(md) ||
|
|
92
|
+
/\b(test command|lint command|build command|verify|run tests|run lint)\b/i.test(md);
|
|
92
93
|
},
|
|
93
94
|
impact: 'critical',
|
|
94
95
|
rating: 5,
|
|
@@ -101,7 +102,7 @@ const TECHNIQUES = {
|
|
|
101
102
|
id: 93001,
|
|
102
103
|
name: 'CLAUDE.md contains a test command',
|
|
103
104
|
check: (ctx) => {
|
|
104
|
-
const md = ctx.
|
|
105
|
+
const md = ctx.claudeMdContent() || '';
|
|
105
106
|
return /npm test|pytest|jest|vitest|cargo test|go test|mix test|rspec/.test(md);
|
|
106
107
|
},
|
|
107
108
|
impact: 'high',
|
|
@@ -115,7 +116,7 @@ const TECHNIQUES = {
|
|
|
115
116
|
id: 93002,
|
|
116
117
|
name: 'CLAUDE.md contains a lint command',
|
|
117
118
|
check: (ctx) => {
|
|
118
|
-
const md = ctx.
|
|
119
|
+
const md = ctx.claudeMdContent() || '';
|
|
119
120
|
return /eslint|prettier|ruff|black|clippy|golangci-lint|rubocop|npm run lint|yarn lint|pnpm lint|bun lint/.test(md);
|
|
120
121
|
},
|
|
121
122
|
impact: 'high',
|
|
@@ -129,7 +130,7 @@ const TECHNIQUES = {
|
|
|
129
130
|
id: 93003,
|
|
130
131
|
name: 'CLAUDE.md contains a build command',
|
|
131
132
|
check: (ctx) => {
|
|
132
|
-
const md = ctx.
|
|
133
|
+
const md = ctx.claudeMdContent() || '';
|
|
133
134
|
return /npm run build|cargo build|go build|make|tsc|gradle build|mvn compile/.test(md);
|
|
134
135
|
},
|
|
135
136
|
impact: 'medium',
|
|
@@ -199,7 +200,7 @@ const TECHNIQUES = {
|
|
|
199
200
|
id: 1039,
|
|
200
201
|
name: 'CLAUDE.md has no embedded API keys',
|
|
201
202
|
check: (ctx) => {
|
|
202
|
-
const md = ctx.
|
|
203
|
+
const md = ctx.claudeMdContent() || '';
|
|
203
204
|
return !/sk-[a-zA-Z0-9]{20,}|xoxb-|AKIA[A-Z0-9]{16}/.test(md);
|
|
204
205
|
},
|
|
205
206
|
impact: 'critical',
|
|
@@ -328,6 +329,7 @@ const TECHNIQUES = {
|
|
|
328
329
|
id: 24,
|
|
329
330
|
name: 'Permission configuration',
|
|
330
331
|
check: (ctx) => {
|
|
332
|
+
// Prefer local (effective config) — any settings file with permissions passes
|
|
331
333
|
const settings = ctx.jsonFile('.claude/settings.local.json') || ctx.jsonFile('.claude/settings.json');
|
|
332
334
|
return !!(settings && settings.permissions);
|
|
333
335
|
},
|
|
@@ -396,7 +398,7 @@ const TECHNIQUES = {
|
|
|
396
398
|
id: 1031,
|
|
397
399
|
name: 'Security review command awareness',
|
|
398
400
|
check: (ctx) => {
|
|
399
|
-
const md = ctx.
|
|
401
|
+
const md = ctx.claudeMdContent() || '';
|
|
400
402
|
return md.includes('security') || md.includes('/security-review');
|
|
401
403
|
},
|
|
402
404
|
impact: 'high',
|
|
@@ -498,7 +500,7 @@ const TECHNIQUES = {
|
|
|
498
500
|
name: 'Frontend design skill for anti-AI-slop',
|
|
499
501
|
check: (ctx) => {
|
|
500
502
|
if (!hasFrontendSignals(ctx)) return null;
|
|
501
|
-
const md = ctx.
|
|
503
|
+
const md = ctx.claudeMdContent() || '';
|
|
502
504
|
return md.includes('frontend_aesthetics') || md.includes('anti-AI-slop') || md.includes('frontend-design');
|
|
503
505
|
},
|
|
504
506
|
impact: 'medium',
|
|
@@ -553,11 +555,13 @@ const TECHNIQUES = {
|
|
|
553
555
|
ciPipeline: {
|
|
554
556
|
id: 260,
|
|
555
557
|
name: 'CI pipeline configured',
|
|
556
|
-
check: (ctx) => ctx.hasDir('.github/workflows')
|
|
558
|
+
check: (ctx) => ctx.hasDir('.github/workflows') || ctx.hasDir('.circleci') ||
|
|
559
|
+
ctx.files.includes('.gitlab-ci.yml') || ctx.files.includes('Jenkinsfile') ||
|
|
560
|
+
ctx.files.includes('.travis.yml') || ctx.files.includes('bitbucket-pipelines.yml'),
|
|
557
561
|
impact: 'high',
|
|
558
562
|
rating: 4,
|
|
559
563
|
category: 'devops',
|
|
560
|
-
fix: 'Add
|
|
564
|
+
fix: 'Add a CI pipeline (GitHub Actions, GitLab CI, CircleCI, etc.) for automated testing and deployment.',
|
|
561
565
|
template: null
|
|
562
566
|
},
|
|
563
567
|
|
|
@@ -658,7 +662,7 @@ const TECHNIQUES = {
|
|
|
658
662
|
id: 568,
|
|
659
663
|
name: 'CLAUDE.md mentions /compact or compaction',
|
|
660
664
|
check: (ctx) => {
|
|
661
|
-
const md = ctx.
|
|
665
|
+
const md = ctx.claudeMdContent() || '';
|
|
662
666
|
return /\/compact|compaction|context.*(limit|manage|budget)/i.test(md);
|
|
663
667
|
},
|
|
664
668
|
impact: 'medium',
|
|
@@ -672,7 +676,7 @@ const TECHNIQUES = {
|
|
|
672
676
|
id: 45,
|
|
673
677
|
name: 'Context management awareness',
|
|
674
678
|
check: (ctx) => {
|
|
675
|
-
const md = ctx.
|
|
679
|
+
const md = ctx.claudeMdContent() || '';
|
|
676
680
|
return /context.*(manage|window|limit|budget|token)/i.test(md);
|
|
677
681
|
},
|
|
678
682
|
impact: 'medium',
|
|
@@ -744,7 +748,7 @@ const TECHNIQUES = {
|
|
|
744
748
|
id: 96,
|
|
745
749
|
name: 'XML tags for structured prompts',
|
|
746
750
|
check: (ctx) => {
|
|
747
|
-
const md = ctx.
|
|
751
|
+
const md = ctx.claudeMdContent() || '';
|
|
748
752
|
// Give credit for XML tags OR well-structured markdown with clear sections
|
|
749
753
|
const hasXml = md.includes('<constraints') || md.includes('<rules') ||
|
|
750
754
|
md.includes('<validation') || md.includes('<instructions');
|
|
@@ -764,7 +768,7 @@ const TECHNIQUES = {
|
|
|
764
768
|
id: 9,
|
|
765
769
|
name: 'CLAUDE.md contains code examples',
|
|
766
770
|
check: (ctx) => {
|
|
767
|
-
const md = ctx.
|
|
771
|
+
const md = ctx.claudeMdContent() || '';
|
|
768
772
|
return (md.match(/```/g) || []).length >= 2;
|
|
769
773
|
},
|
|
770
774
|
impact: 'high',
|
|
@@ -778,8 +782,8 @@ const TECHNIQUES = {
|
|
|
778
782
|
id: 10,
|
|
779
783
|
name: 'CLAUDE.md defines a role or persona',
|
|
780
784
|
check: (ctx) => {
|
|
781
|
-
const md = ctx.
|
|
782
|
-
return
|
|
785
|
+
const md = ctx.claudeMdContent() || '';
|
|
786
|
+
return /^you are a |^your role is|^act as a |persona:|behave as a /im.test(md);
|
|
783
787
|
},
|
|
784
788
|
impact: 'medium',
|
|
785
789
|
rating: 4,
|
|
@@ -792,7 +796,7 @@ const TECHNIQUES = {
|
|
|
792
796
|
id: 9601,
|
|
793
797
|
name: 'XML constraint blocks in CLAUDE.md',
|
|
794
798
|
check: (ctx) => {
|
|
795
|
-
const md = ctx.
|
|
799
|
+
const md = ctx.claudeMdContent() || '';
|
|
796
800
|
return /<constraints|<rules|<requirements|<boundaries/i.test(md);
|
|
797
801
|
},
|
|
798
802
|
impact: 'high',
|
|
@@ -810,10 +814,10 @@ const TECHNIQUES = {
|
|
|
810
814
|
id: 1102,
|
|
811
815
|
name: 'Claude Code Channels awareness',
|
|
812
816
|
check: (ctx) => {
|
|
813
|
-
const md = ctx.
|
|
817
|
+
const md = ctx.claudeMdContent() || '';
|
|
814
818
|
const settings = ctx.jsonFile('.claude/settings.local.json') || ctx.jsonFile('.claude/settings.json');
|
|
815
819
|
const settingsStr = JSON.stringify(settings || {});
|
|
816
|
-
return
|
|
820
|
+
return /\bchannels?\b.*\b(telegram|discord|imessage|slack|bridge)\b|\b(telegram|discord|imessage|slack|bridge)\b.*\bchannels?\b/i.test(md) || settingsStr.includes('channels');
|
|
817
821
|
},
|
|
818
822
|
impact: 'low',
|
|
819
823
|
rating: 3,
|
|
@@ -831,7 +835,7 @@ const TECHNIQUES = {
|
|
|
831
835
|
id: 2001,
|
|
832
836
|
name: 'CLAUDE.md mentions current Claude features',
|
|
833
837
|
check: (ctx) => {
|
|
834
|
-
const md = ctx.
|
|
838
|
+
const md = ctx.claudeMdContent() || '';
|
|
835
839
|
if (md.length < 50) return false; // too short to evaluate
|
|
836
840
|
// Check for awareness of features from 2025+
|
|
837
841
|
const modernFeatures = ['hook', 'skill', 'agent', 'subagent', 'mcp', 'compact', '/clear', 'extended thinking', 'tool_use', 'worktree'];
|
|
@@ -845,13 +849,14 @@ const TECHNIQUES = {
|
|
|
845
849
|
template: null
|
|
846
850
|
},
|
|
847
851
|
|
|
852
|
+
// claudeMdNotOverlong removed — duplicate of underlines200 (id 681)
|
|
853
|
+
|
|
848
854
|
claudeMdNotOverlong: {
|
|
849
855
|
id: 2002,
|
|
850
856
|
name: 'CLAUDE.md is concise (under 200 lines)',
|
|
851
857
|
check: (ctx) => {
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
return md.split('\n').length <= 200;
|
|
858
|
+
// Defer to underlines200 — this check always returns null (skipped)
|
|
859
|
+
return null;
|
|
855
860
|
},
|
|
856
861
|
impact: 'medium',
|
|
857
862
|
rating: 4,
|
|
@@ -864,12 +869,20 @@ const TECHNIQUES = {
|
|
|
864
869
|
id: 2003,
|
|
865
870
|
name: 'CLAUDE.md has no obvious contradictions',
|
|
866
871
|
check: (ctx) => {
|
|
867
|
-
const md = ctx.
|
|
872
|
+
const md = ctx.claudeMdContent();
|
|
868
873
|
if (!md || md.length < 50) return false; // no CLAUDE.md or too short = not passing
|
|
869
874
|
// Check for common contradictions
|
|
870
|
-
|
|
871
|
-
const
|
|
872
|
-
|
|
875
|
+
// Check for contradictions on the SAME topic (same line or adjacent sentence)
|
|
876
|
+
const lines = md.split('\n');
|
|
877
|
+
let hasContradiction = false;
|
|
878
|
+
for (const line of lines) {
|
|
879
|
+
if (/\balways\b.*\bnever\b|\bnever\b.*\balways\b/i.test(line)) {
|
|
880
|
+
hasContradiction = true;
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
const hasBothStyles = /\buse tabs\b/i.test(md) && /\buse spaces\b/i.test(md);
|
|
885
|
+
return !hasContradiction && !hasBothStyles;
|
|
873
886
|
},
|
|
874
887
|
impact: 'high',
|
|
875
888
|
rating: 4,
|
|
@@ -940,14 +953,15 @@ const TECHNIQUES = {
|
|
|
940
953
|
|
|
941
954
|
securityReviewInWorkflow: {
|
|
942
955
|
id: 2008,
|
|
943
|
-
name: '/security-review
|
|
956
|
+
name: '/security-review command or workflow',
|
|
944
957
|
check: (ctx) => {
|
|
945
|
-
const md = ctx.fileContent('CLAUDE.md') || '';
|
|
946
958
|
const hasCommand = ctx.hasDir('.claude/commands') &&
|
|
947
959
|
(ctx.dirFiles('.claude/commands') || []).some(f => f.includes('security') || f.includes('review'));
|
|
948
|
-
|
|
960
|
+
const md = ctx.claudeMdContent() || '';
|
|
961
|
+
const hasExplicitRef = /\/security-review|security review command|security workflow/i.test(md);
|
|
962
|
+
return hasCommand || hasExplicitRef;
|
|
949
963
|
},
|
|
950
|
-
impact: '
|
|
964
|
+
impact: 'medium',
|
|
951
965
|
rating: 4,
|
|
952
966
|
category: 'quality-deep',
|
|
953
967
|
fix: 'Claude Code has built-in /security-review (OWASP Top 10). Add it to your workflow or create a /security command.',
|
|
@@ -959,7 +973,7 @@ const TECHNIQUES = {
|
|
|
959
973
|
id: 2010,
|
|
960
974
|
name: 'Test coverage or strategy mentioned',
|
|
961
975
|
check: (ctx) => {
|
|
962
|
-
const md = ctx.
|
|
976
|
+
const md = ctx.claudeMdContent() || '';
|
|
963
977
|
return /coverage|test.*strateg|e2e|integration test|unit test/i.test(md);
|
|
964
978
|
},
|
|
965
979
|
impact: 'medium', rating: 3, category: 'quality',
|
|
@@ -991,7 +1005,7 @@ const TECHNIQUES = {
|
|
|
991
1005
|
id: 2012,
|
|
992
1006
|
name: 'Auto-memory or memory management mentioned',
|
|
993
1007
|
check: (ctx) => {
|
|
994
|
-
const md = ctx.
|
|
1008
|
+
const md = ctx.claudeMdContent() || '';
|
|
995
1009
|
return /auto.?memory|memory.*manage|remember|persistent.*context/i.test(md);
|
|
996
1010
|
},
|
|
997
1011
|
impact: 'low', rating: 3, category: 'memory',
|
|
@@ -1004,7 +1018,7 @@ const TECHNIQUES = {
|
|
|
1004
1018
|
id: 2013,
|
|
1005
1019
|
name: 'Sandbox or isolation mentioned',
|
|
1006
1020
|
check: (ctx) => {
|
|
1007
|
-
const md = ctx.
|
|
1021
|
+
const md = ctx.claudeMdContent() || '';
|
|
1008
1022
|
const settings = ctx.jsonFile('.claude/settings.json') || {};
|
|
1009
1023
|
return /sandbox|isolat/i.test(md) || !!settings.sandbox;
|
|
1010
1024
|
},
|
|
@@ -1047,7 +1061,7 @@ const TECHNIQUES = {
|
|
|
1047
1061
|
id: 2016,
|
|
1048
1062
|
name: 'Effort level or thinking configuration',
|
|
1049
1063
|
check: (ctx) => {
|
|
1050
|
-
const md = ctx.
|
|
1064
|
+
const md = ctx.claudeMdContent() || '';
|
|
1051
1065
|
const shared = ctx.jsonFile('.claude/settings.json') || {};
|
|
1052
1066
|
const local = ctx.jsonFile('.claude/settings.local.json') || {};
|
|
1053
1067
|
return /effort|thinking/i.test(md) || shared.effortLevel || local.effortLevel ||
|
|
@@ -1074,7 +1088,7 @@ const TECHNIQUES = {
|
|
|
1074
1088
|
id: 2018,
|
|
1075
1089
|
name: 'Worktree or parallel sessions mentioned',
|
|
1076
1090
|
check: (ctx) => {
|
|
1077
|
-
const md = ctx.
|
|
1091
|
+
const md = ctx.claudeMdContent() || '';
|
|
1078
1092
|
const shared = ctx.jsonFile('.claude/settings.json') || {};
|
|
1079
1093
|
return /worktree|parallel.*session/i.test(md) || !!shared.worktree;
|
|
1080
1094
|
},
|
|
@@ -1088,7 +1102,7 @@ const TECHNIQUES = {
|
|
|
1088
1102
|
id: 2019,
|
|
1089
1103
|
name: 'CLAUDE.md includes "do not" instructions',
|
|
1090
1104
|
check: (ctx) => {
|
|
1091
|
-
const md = ctx.
|
|
1105
|
+
const md = ctx.claudeMdContent() || '';
|
|
1092
1106
|
return /do not|don't|never|avoid|must not/i.test(md);
|
|
1093
1107
|
},
|
|
1094
1108
|
impact: 'medium', rating: 4, category: 'prompting',
|
|
@@ -1100,8 +1114,8 @@ const TECHNIQUES = {
|
|
|
1100
1114
|
id: 2020,
|
|
1101
1115
|
name: 'CLAUDE.md includes output or style guidance',
|
|
1102
1116
|
check: (ctx) => {
|
|
1103
|
-
const md = ctx.
|
|
1104
|
-
return /style|
|
|
1117
|
+
const md = ctx.claudeMdContent() || '';
|
|
1118
|
+
return /coding style|naming convention|code style|style guide|formatting rules|\bprefer\b.*\b(single|double|tabs|spaces|camel|snake|kebab|named|default|const|let|arrow|function)\b/i.test(md);
|
|
1105
1119
|
},
|
|
1106
1120
|
impact: 'medium', rating: 3, category: 'prompting',
|
|
1107
1121
|
fix: 'Add coding style and naming conventions to CLAUDE.md so Claude matches your project patterns.',
|
|
@@ -1127,7 +1141,7 @@ const TECHNIQUES = {
|
|
|
1127
1141
|
id: 2022,
|
|
1128
1142
|
name: 'CLAUDE.md describes what the project does',
|
|
1129
1143
|
check: (ctx) => {
|
|
1130
|
-
const md = ctx.
|
|
1144
|
+
const md = ctx.claudeMdContent() || '';
|
|
1131
1145
|
return /what.*does|overview|purpose|about|description|project.*is/i.test(md) && md.length > 100;
|
|
1132
1146
|
},
|
|
1133
1147
|
impact: 'high', rating: 4, category: 'memory',
|
|
@@ -1139,7 +1153,7 @@ const TECHNIQUES = {
|
|
|
1139
1153
|
id: 2023,
|
|
1140
1154
|
name: 'CLAUDE.md documents directory structure',
|
|
1141
1155
|
check: (ctx) => {
|
|
1142
|
-
const md = ctx.
|
|
1156
|
+
const md = ctx.claudeMdContent() || '';
|
|
1143
1157
|
return /src\/|app\/|lib\/|structure|director|folder/i.test(md);
|
|
1144
1158
|
},
|
|
1145
1159
|
impact: 'medium', rating: 4, category: 'memory',
|
|
@@ -1265,15 +1279,15 @@ const TECHNIQUES = {
|
|
|
1265
1279
|
id: 2009,
|
|
1266
1280
|
name: 'No deprecated patterns detected',
|
|
1267
1281
|
check: (ctx) => {
|
|
1268
|
-
const md = ctx.
|
|
1282
|
+
const md = ctx.claudeMdContent();
|
|
1269
1283
|
if (!md) return false; // no CLAUDE.md = not passing
|
|
1270
1284
|
// Check for patterns deprecated in Claude 4.x
|
|
1271
|
-
const
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1285
|
+
const deprecatedPatterns = [
|
|
1286
|
+
/\bprefill\b/i, // deprecated API pattern in 4.6
|
|
1287
|
+
/\bclaude-3-opus\b/i, /\bclaude-3-sonnet\b/i, /\bclaude-3-haiku\b/i, // old model names
|
|
1288
|
+
/\bhuman_prompt\b/i, /\bassistant_prompt\b/i, // old API format
|
|
1275
1289
|
];
|
|
1276
|
-
return !
|
|
1290
|
+
return !deprecatedPatterns.some(p => p.test(md));
|
|
1277
1291
|
},
|
|
1278
1292
|
impact: 'medium',
|
|
1279
1293
|
rating: 3,
|