claude-launchpad 0.4.3 → 0.5.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/README.md CHANGED
@@ -81,7 +81,7 @@ The core of the tool. Runs 7 analyzers against your `.claude/` directory and `CL
81
81
  | **Settings** | No hooks configured, dangerous tool access without safety nets |
82
82
  | **Hooks** | Missing auto-format on save, no .env file protection, no security gates, no PostCompact hook |
83
83
  | **Rules** | Dead rule files, stale references, empty configs |
84
- | **Permissions** | Bash auto-allowed without security hooks, no force-push protection |
84
+ | **Permissions** | Credential file exposure (~/.ssh, ~/.aws, ~/.npmrc), blanket Bash approval, bypass mode unprotected, sandbox disabled, .env gap between hooks and .claudeignore, no force-push protection |
85
85
  | **MCP Servers** | Invalid transport configs, missing commands/URLs |
86
86
 
87
87
  Output looks like this:
@@ -132,7 +132,7 @@ Detects your project and generates Claude Code config that fits. No templates, n
132
132
  **What you get (6 files):**
133
133
  - `CLAUDE.md` — your stack, commands, conventions, guardrails, memory management instructions
134
134
  - `TASKS.md` — sprint tracking, session continuity, deferred issues parking
135
- - `.claude/settings.json` — `$schema` for IDE autocomplete, `permissions.deny` for security, hooks for .env protection + destructive command blocking + auto-format + PostCompact context re-injection
135
+ - `.claude/settings.json` — `$schema` for IDE autocomplete, `permissions.deny` for credential + secret protection, sandbox enabled, bypass mode disabled, hooks for .env protection + destructive command blocking + auto-format + PostCompact context re-injection
136
136
  - `.claude/.gitignore` — prevents local settings and plans from being committed
137
137
  - `.claudeignore` — language-specific ignore patterns
138
138
  - `.claude/rules/conventions.md` — language-specific starter rules
@@ -154,23 +154,11 @@ Stays under the 120-instruction budget. Overflows detailed content to `.claude/r
154
154
  The part nobody else has built. Runs Claude against real test scenarios and scores the results.
155
155
 
156
156
  ```bash
157
- # Run only security tests (4 scenarios)
158
- claude-launchpad eval --suite security
159
-
160
- # Run only convention tests (5 scenarios)
161
- claude-launchpad eval --suite conventions
162
-
163
- # Run only workflow tests (2 scenarios)
164
- claude-launchpad eval --suite workflow
165
-
166
- # Run everything (11 scenarios)
157
+ # Interactive mode pick suite, runs, and model
167
158
  claude-launchpad eval
168
159
 
169
- # Use a cheaper model
170
- claude-launchpad eval --suite security --model haiku
171
-
172
- # One run per scenario (fastest)
173
- claude-launchpad eval --suite security --runs 1
160
+ # Or pass flags directly
161
+ claude-launchpad eval --suite security --runs 1 --model haiku
174
162
  ```
175
163
 
176
164
  Each scenario creates an isolated sandbox with your full Claude Code config (settings.json, rules, hooks, .claudeignore) copied in, runs Claude with a task, and checks if your configuration made Claude follow the rules:
@@ -192,9 +180,9 @@ Results are saved to `.claude/eval/` as structured markdown — you can feed the
192
180
 
193
181
  | Suite | Scenarios | What it tests |
194
182
  |---|---|---|
195
- | `security` | 4 | SQL injection, .env protection, secret exposure, input validation |
183
+ | `security` | 6 | SQL injection, .env protection, secret exposure, input validation, credential read, sandbox escape |
196
184
  | `conventions` | 5 | Error handling, immutability, file size, naming, no hardcoded values |
197
- | `workflow` | 2 | Git conventions, session continuity |
185
+ | `workflow` | 4 | Git conventions, session continuity, memory persistence, deferred tracking |
198
186
 
199
187
  **All eval flags:**
200
188
 
@@ -241,7 +229,7 @@ Then use `/launchpad:doctor`, `/launchpad:init`, `/launchpad:enhance`, `/launchp
241
229
 
242
230
  **Doctor** reads your files and runs static analysis. No API calls. No network. No cost.
243
231
 
244
- **Init** scans manifest files (package.json, go.mod, pyproject.toml, etc.), detects your stack, and generates 6 files: CLAUDE.md (with memory management instructions), TASKS.md (with deferred issues section), settings.json (with $schema, permissions.deny, hooks including PostCompact for context re-injection), .claude/.gitignore, .claudeignore, and language-specific rules. Formatter hooks use hardcoded safe commands only.
232
+ **Init** scans manifest files (package.json, go.mod, pyproject.toml, etc.), detects your stack, and generates 6 files: CLAUDE.md (with memory management instructions), TASKS.md (with deferred issues section), settings.json (with credential deny rules, sandbox enabled, bypass mode disabled, hooks including PostCompact), .claude/.gitignore, .claudeignore, and language-specific rules. Formatter hooks use hardcoded safe commands only.
245
233
 
246
234
  **Enhance** spawns `claude "prompt"` as an interactive child process. You see Claude's full UI. No data passes through the tool — it just launches Claude with a task.
247
235
 
package/dist/cli.js CHANGED
@@ -126,7 +126,7 @@ async function readJsonOrNull(path) {
126
126
  import { join, basename } from "path";
127
127
  async function detectProject(root) {
128
128
  const name = basename(root);
129
- const [pkgJson, goMod, pyProject, gemfile, cargo, pubspec, composerJson, pomXml, buildGradle, packageSwift, mixExs, csproj, lockfiles] = await Promise.all([
129
+ const [pkgJson, goMod, pyProject, gemfile, cargo, pubspec, composerJson, pomXml, buildGradleGroovy, buildGradleKts, packageSwift, mixExs, csproj, lockfiles] = await Promise.all([
130
130
  readJsonOrNull(join(root, "package.json")),
131
131
  fileExists(join(root, "go.mod")),
132
132
  readFileOrNull(join(root, "pyproject.toml")),
@@ -135,12 +135,14 @@ async function detectProject(root) {
135
135
  fileExists(join(root, "pubspec.yaml")),
136
136
  readJsonOrNull(join(root, "composer.json")),
137
137
  fileExists(join(root, "pom.xml")),
138
- fileExists(join(root, "build.gradle")) || fileExists(join(root, "build.gradle.kts")),
138
+ fileExists(join(root, "build.gradle")),
139
+ fileExists(join(root, "build.gradle.kts")),
139
140
  fileExists(join(root, "Package.swift")),
140
141
  fileExists(join(root, "mix.exs")),
141
142
  globExists(root, "*.csproj"),
142
143
  detectLockfiles(root)
143
144
  ]);
145
+ const buildGradle = buildGradleGroovy || buildGradleKts;
144
146
  const manifests = {
145
147
  pkgJson,
146
148
  goMod,
@@ -402,6 +404,13 @@ function generateSettings(detected) {
402
404
  if (formatHook) {
403
405
  postToolUse.push(formatHook);
404
406
  }
407
+ const sessionStart = [{
408
+ matcher: "startup|resume",
409
+ hooks: [{
410
+ type: "command",
411
+ command: "cat TASKS.md 2>/dev/null; exit 0"
412
+ }]
413
+ }];
405
414
  const postCompact = [{
406
415
  matcher: "",
407
416
  hooks: [{
@@ -410,6 +419,7 @@ function generateSettings(detected) {
410
419
  }]
411
420
  }];
412
421
  const hooks = {};
422
+ hooks.SessionStart = sessionStart;
413
423
  if (preToolUse.length > 0) hooks.PreToolUse = preToolUse;
414
424
  if (postToolUse.length > 0) hooks.PostToolUse = postToolUse;
415
425
  hooks.PostCompact = postCompact;
@@ -421,10 +431,18 @@ function generateSettings(detected) {
421
431
  "Bash(rm -rf ~)",
422
432
  "Read(.env)",
423
433
  "Read(.env.*)",
424
- "Read(secrets/**)"
434
+ "Read(secrets/**)",
435
+ "Read(~/.ssh/*)",
436
+ "Read(~/.aws/*)",
437
+ "Read(~/.npmrc)"
425
438
  ]
426
439
  },
427
- hooks
440
+ hooks,
441
+ disableBypassPermissionsMode: "disable",
442
+ sandbox: {
443
+ enabled: true,
444
+ failIfUnavailable: true
445
+ }
428
446
  };
429
447
  }
430
448
  var SAFE_FORMATTERS = {
@@ -730,13 +748,14 @@ var RULES_DIR = "rules";
730
748
  async function parseClaudeConfig(projectRoot) {
731
749
  const root = resolve(projectRoot);
732
750
  const claudeDir = join3(root, CLAUDE_DIR);
733
- const [claudeMd, settings, hooks, rules, mcpServers, skills] = await Promise.all([
751
+ const [claudeMd, settings, hooks, rules, mcpServers, skills, claudeignore] = await Promise.all([
734
752
  readClaudeMd(root),
735
753
  readSettings(claudeDir),
736
754
  readHooks(claudeDir),
737
755
  readRules(claudeDir),
738
756
  readMcpServers(claudeDir),
739
- readSkills(claudeDir)
757
+ readSkills(claudeDir),
758
+ readFileOrNull(join3(root, ".claudeignore"))
740
759
  ]);
741
760
  const instructionCount = claudeMd ? countInstructions(claudeMd) : 0;
742
761
  return {
@@ -748,7 +767,9 @@ async function parseClaudeConfig(projectRoot) {
748
767
  hooks,
749
768
  rules,
750
769
  mcpServers,
751
- skills
770
+ skills,
771
+ claudeignorePath: claudeignore !== null ? join3(root, ".claudeignore") : null,
772
+ claudeignoreContent: claudeignore
752
773
  };
753
774
  }
754
775
  async function readClaudeMd(root) {
@@ -797,7 +818,8 @@ async function readHooks(claudeDir) {
797
818
  event,
798
819
  type: h.type ?? "command",
799
820
  matcher,
800
- command: h.command
821
+ command: h.command,
822
+ timeout: h.timeout
801
823
  });
802
824
  }
803
825
  } else {
@@ -805,7 +827,8 @@ async function readHooks(claudeDir) {
805
827
  event,
806
828
  type: g.type ?? "command",
807
829
  matcher,
808
- command: g.command
830
+ command: g.command,
831
+ timeout: g.timeout
809
832
  });
810
833
  }
811
834
  }
@@ -968,6 +991,44 @@ async function analyzeSettings(config) {
968
991
  fix: "Add PreToolUse hooks for security or remove allowedTools to use interactive prompting"
969
992
  });
970
993
  }
994
+ if (config.settings.includeCoAuthoredBy !== void 0) {
995
+ issues.push({
996
+ analyzer: "Settings",
997
+ severity: "low",
998
+ message: 'Deprecated includeCoAuthoredBy \u2014 use attribution: { commit: "", pr: "" } instead',
999
+ fix: "Replace includeCoAuthoredBy with the attribution object in settings.json"
1000
+ });
1001
+ }
1002
+ if (!config.settings.claudeMdExcludes) {
1003
+ issues.push({
1004
+ analyzer: "Settings",
1005
+ severity: "info",
1006
+ message: "No claudeMdExcludes configured \u2014 consider adding this if you have a monorepo"
1007
+ });
1008
+ }
1009
+ const broadMatchers = ["Bash", "Write", "Edit", "Read"];
1010
+ const hooksWithoutTimeout = config.hooks.filter(
1011
+ (h) => !h.timeout && broadMatchers.some((m) => h.matcher?.includes(m))
1012
+ );
1013
+ if (hooksWithoutTimeout.length > 0) {
1014
+ issues.push({
1015
+ analyzer: "Settings",
1016
+ severity: "low",
1017
+ message: `${hooksWithoutTimeout.length} hook(s) on broad matchers without timeout \u2014 defaults to 60s per invocation`,
1018
+ fix: "Add timeout (in seconds) to hooks on Bash, Write, Edit, or Read matchers"
1019
+ });
1020
+ }
1021
+ if (config.settings.autoMemoryEnabled === false) {
1022
+ const hasMemorySection = config.claudeMdContent?.includes("## Memory") ?? false;
1023
+ if (!hasMemorySection) {
1024
+ issues.push({
1025
+ analyzer: "Settings",
1026
+ severity: "medium",
1027
+ message: "Auto-memory is disabled with no manual memory strategy in CLAUDE.md",
1028
+ fix: "Re-enable autoMemoryEnabled or add a ## Memory section to CLAUDE.md"
1029
+ });
1030
+ }
1031
+ }
971
1032
  const actionableCount = issues.filter((i) => i.severity !== "info").length;
972
1033
  const score = Math.max(0, 100 - actionableCount * 20);
973
1034
  return { name: "Settings", issues, score };
@@ -1027,7 +1088,16 @@ async function analyzeHooks(config) {
1027
1088
  fix: "Add a PostCompact hook that re-injects TASKS.md after compaction"
1028
1089
  });
1029
1090
  }
1030
- const score = Math.max(0, 100 - issues.length * 20);
1091
+ const hasSessionStart = hooks.some((h) => h.event === "SessionStart");
1092
+ if (!hasSessionStart) {
1093
+ issues.push({
1094
+ analyzer: "Hooks",
1095
+ severity: "low",
1096
+ message: "No SessionStart hook \u2014 session starts without project context loaded",
1097
+ fix: "Add a SessionStart hook that injects TASKS.md at startup"
1098
+ });
1099
+ }
1100
+ const score = Math.max(0, 100 - issues.length * 15);
1031
1101
  return { name: "Hooks", issues, score };
1032
1102
  }
1033
1103
 
@@ -1088,10 +1158,63 @@ async function analyzeRules(config) {
1088
1158
  // src/commands/doctor/analyzers/permissions.ts
1089
1159
  async function analyzePermissions(config) {
1090
1160
  const issues = [];
1161
+ const settings = config.settings;
1162
+ const permissions = settings?.permissions;
1163
+ const denyList = permissions?.deny ?? [];
1164
+ const allowList = permissions?.allow ?? [];
1165
+ const credentialPatterns = ["Read(~/.ssh/*)", "Read(~/.aws/*)", "Read(~/.npmrc)"];
1166
+ const missingCreds = credentialPatterns.filter((p) => !denyList.includes(p));
1167
+ if (missingCreds.length > 0) {
1168
+ issues.push({
1169
+ analyzer: "Permissions",
1170
+ severity: "high",
1171
+ message: `Credential files not blocked: ${missingCreds.join(", ")} \u2014 Claude can read SSH keys, AWS creds, or npm tokens`,
1172
+ fix: "Add Read(~/.ssh/*), Read(~/.aws/*), Read(~/.npmrc) to permissions.deny"
1173
+ });
1174
+ }
1175
+ const hasBlanketBash = allowList.some((a) => a === "Bash" || a.startsWith("Bash") && !a.includes("("));
1176
+ if (hasBlanketBash) {
1177
+ issues.push({
1178
+ analyzer: "Permissions",
1179
+ severity: "high",
1180
+ message: "Bash is blanket-allowed without pattern restriction \u2014 all shell commands are auto-approved",
1181
+ fix: "Replace blanket Bash with scoped patterns like Bash(npm test) or remove it"
1182
+ });
1183
+ }
1184
+ if (settings?.disableBypassPermissionsMode !== "disable") {
1185
+ issues.push({
1186
+ analyzer: "Permissions",
1187
+ severity: "high",
1188
+ message: "Bypass permissions mode not disabled \u2014 --dangerously-skip-permissions bypasses all checks",
1189
+ fix: 'Add "disableBypassPermissionsMode": "disable" to settings.json'
1190
+ });
1191
+ }
1192
+ const sandbox = settings?.sandbox;
1193
+ if (sandbox?.enabled !== true) {
1194
+ issues.push({
1195
+ analyzer: "Permissions",
1196
+ severity: "medium",
1197
+ message: "Sandbox not enabled \u2014 hooks block tool calls but not subprocess access (e.g. cat .env via Bash)",
1198
+ fix: 'Add "sandbox": { "enabled": true, "failIfUnavailable": true } to settings.json'
1199
+ });
1200
+ }
1201
+ const hasEnvHook = config.hooks.some((h) => h.command?.includes(".env"));
1202
+ if (hasEnvHook && config.claudeignoreContent !== null) {
1203
+ const lines = config.claudeignoreContent.split("\n").map((l) => l.trim());
1204
+ const hasEnvInIgnore = lines.some((l) => l === ".env" || l === ".env.*" || l === ".env*");
1205
+ if (!hasEnvInIgnore) {
1206
+ issues.push({
1207
+ analyzer: "Permissions",
1208
+ severity: "medium",
1209
+ message: ".env is protected by hooks but not in .claudeignore \u2014 cat .env via Bash bypasses hooks",
1210
+ fix: "Add .env to .claudeignore for defense in depth"
1211
+ });
1212
+ }
1213
+ }
1091
1214
  const hasBashSecurity = config.hooks.some(
1092
1215
  (h) => h.event === "PreToolUse" && (h.matcher?.includes("Bash") || !h.matcher)
1093
1216
  );
1094
- const bashAllowed = config.settings?.allowedTools;
1217
+ const bashAllowed = settings?.allowedTools;
1095
1218
  const hasBashAutoAllow = bashAllowed?.some(
1096
1219
  (t) => typeof t === "string" && t.toLowerCase().includes("bash")
1097
1220
  );
@@ -1100,7 +1223,7 @@ async function analyzePermissions(config) {
1100
1223
  analyzer: "Permissions",
1101
1224
  severity: "high",
1102
1225
  message: "Bash is auto-allowed without a security hook \u2014 dangerous commands could run unchecked",
1103
- fix: "Add a PreToolUse hook for Bash that blocks destructive commands (rm -rf, git push --force)"
1226
+ fix: "Add a PreToolUse hook for Bash that blocks destructive commands"
1104
1227
  });
1105
1228
  }
1106
1229
  const hasForceProtection = config.hooks.some(
@@ -1125,7 +1248,7 @@ async function analyzePermissions(config) {
1125
1248
  });
1126
1249
  }
1127
1250
  }
1128
- const score = Math.max(0, 100 - issues.length * 20);
1251
+ const score = Math.max(0, 100 - issues.length * 15);
1129
1252
  return { name: "Permissions", issues, score };
1130
1253
  }
1131
1254
 
@@ -1303,7 +1426,12 @@ var FIX_TABLE = [
1303
1426
  { analyzer: "Rules", match: "No .claude/rules/", fix: (root) => createStarterRules(root) },
1304
1427
  { analyzer: "Quality", match: "Memory", fix: (root) => addClaudeMdSection(root, "## Memory & Learnings", "Use the built-in memory system to persist knowledge across sessions:\n- **Save immediately** when you discover: a non-obvious fix, a gotcha, an external resource, a decision with context that would be lost, or a known issue to fix later\n- **Categories**: `decision` (why X over Y), `gotcha` (non-obvious pitfall), `deferred` (known issue, not urgent), `reference` (where to find things)\n- **Where**: project memory for this repo, global memory for cross-project learnings\n- **Format**: one fact per memory, include date and why \u2014 not just what\n- **Prune**: check if a memory on this topic exists before saving \u2014 update, don't duplicate\n- Before starting work, check memory for relevant context from previous sessions") },
1305
1428
  { analyzer: "Hooks", match: "PostCompact", fix: (root) => addPostCompactHook(root) },
1306
- { analyzer: "Permissions", match: "force-push", fix: (root) => addForcePushProtection(root) }
1429
+ { analyzer: "Permissions", match: "force-push", fix: (root) => addForcePushProtection(root) },
1430
+ { analyzer: "Permissions", match: "Credential files not blocked", fix: (root) => addCredentialDenyRules(root) },
1431
+ { analyzer: "Permissions", match: "Bypass permissions mode", fix: (root) => addBypassDisable(root) },
1432
+ { analyzer: "Permissions", match: "Sandbox not enabled", fix: (root) => addSandboxSettings(root) },
1433
+ { analyzer: "Permissions", match: ".env is protected by hooks but not in .claudeignore", fix: (root) => addEnvToClaudeignore(root) },
1434
+ { analyzer: "Settings", match: "Deprecated includeCoAuthoredBy", fix: (root) => migrateAttribution(root) }
1307
1435
  ];
1308
1436
  async function tryFix(issue, root, detected) {
1309
1437
  const entry = FIX_TABLE.find(
@@ -1405,6 +1533,58 @@ async function addPostCompactHook(root) {
1405
1533
  log.success("Added PostCompact hook (re-injects TASKS.md after compaction)");
1406
1534
  return true;
1407
1535
  }
1536
+ async function migrateAttribution(root) {
1537
+ const settings = await readSettingsJson(root);
1538
+ if (settings.includeCoAuthoredBy === void 0) return false;
1539
+ const { includeCoAuthoredBy: _, ...rest } = settings;
1540
+ const updated = { ...rest, attribution: { commit: "", pr: "" } };
1541
+ await writeSettingsJson(root, updated);
1542
+ log.success("Migrated includeCoAuthoredBy \u2192 attribution object");
1543
+ return true;
1544
+ }
1545
+ async function addCredentialDenyRules(root) {
1546
+ const settings = await readSettingsJson(root);
1547
+ const permissions = settings.permissions ?? {};
1548
+ const deny = permissions.deny ?? [];
1549
+ const toAdd = ["Read(~/.ssh/*)", "Read(~/.aws/*)", "Read(~/.npmrc)"];
1550
+ const missing = toAdd.filter((p) => !deny.includes(p));
1551
+ if (missing.length === 0) return false;
1552
+ settings.permissions = { ...permissions, deny: [...deny, ...missing] };
1553
+ await writeSettingsJson(root, settings);
1554
+ log.success("Added credential deny rules (SSH, AWS, npm)");
1555
+ return true;
1556
+ }
1557
+ async function addBypassDisable(root) {
1558
+ const settings = await readSettingsJson(root);
1559
+ if (settings.disableBypassPermissionsMode === "disable") return false;
1560
+ settings.disableBypassPermissionsMode = "disable";
1561
+ await writeSettingsJson(root, settings);
1562
+ log.success("Added disableBypassPermissionsMode: disable");
1563
+ return true;
1564
+ }
1565
+ async function addSandboxSettings(root) {
1566
+ const settings = await readSettingsJson(root);
1567
+ const sandbox = settings.sandbox;
1568
+ if (sandbox?.enabled === true) return false;
1569
+ settings.sandbox = { enabled: true, failIfUnavailable: true };
1570
+ await writeSettingsJson(root, settings);
1571
+ log.success("Enabled sandbox with failIfUnavailable");
1572
+ return true;
1573
+ }
1574
+ async function addEnvToClaudeignore(root) {
1575
+ const ignorePath = join5(root, ".claudeignore");
1576
+ let content;
1577
+ try {
1578
+ content = await readFile4(ignorePath, "utf-8");
1579
+ } catch {
1580
+ return false;
1581
+ }
1582
+ const lines = content.split("\n").map((l) => l.trim());
1583
+ if (lines.some((l) => l === ".env" || l === ".env.*" || l === ".env*")) return false;
1584
+ await writeFile2(ignorePath, content.trimEnd() + "\n.env\n.env.*\n");
1585
+ log.success("Added .env to .claudeignore");
1586
+ return true;
1587
+ }
1408
1588
  async function addClaudeMdSection(root, heading, content) {
1409
1589
  const claudeMdPath = join5(root, "CLAUDE.md");
1410
1590
  let existing;
@@ -1590,7 +1770,19 @@ function createDoctorCommand() {
1590
1770
  const { fixed, skipped } = await applyFixes(fixable, opts.path);
1591
1771
  log.blank();
1592
1772
  if (fixed > 0) {
1593
- log.success(`Applied ${fixed} fix(es). Run \`claude-launchpad doctor\` again to see your new score.`);
1773
+ log.success(`Applied ${fixed} fix(es). Re-scanning...`);
1774
+ log.blank();
1775
+ const updatedConfig = await parseClaudeConfig(opts.path);
1776
+ const updatedResults = await Promise.all([
1777
+ analyzeBudget(updatedConfig),
1778
+ analyzeQuality(updatedConfig),
1779
+ analyzeSettings(updatedConfig),
1780
+ analyzeHooks(updatedConfig),
1781
+ analyzeRules(updatedConfig),
1782
+ analyzePermissions(updatedConfig),
1783
+ analyzeMcp(updatedConfig)
1784
+ ]);
1785
+ renderDoctorReport(updatedResults);
1594
1786
  }
1595
1787
  if (skipped > 0) {
1596
1788
  log.info(`${skipped} issue(s) require manual intervention.`);
@@ -1608,6 +1800,7 @@ function createDoctorCommand() {
1608
1800
 
1609
1801
  // src/commands/eval/index.ts
1610
1802
  import { Command as Command3 } from "commander";
1803
+ import { select } from "@inquirer/prompts";
1611
1804
  import ora from "ora";
1612
1805
  import chalk2 from "chalk";
1613
1806
  import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
@@ -2017,6 +2210,35 @@ async function listAllFiles(dir) {
2017
2210
  function createEvalCommand() {
2018
2211
  return new Command3("eval").description("Test your Claude Code config against eval scenarios").option("-s, --suite <suite>", "Eval suite to run (e.g., security, conventions, workflow)").option("-p, --path <path>", "Project root path", process.cwd()).option("--scenarios <path>", "Custom scenarios directory").option("--runs <n>", "Runs per scenario (default: 3)", "3").option("--timeout <ms>", "Timeout per run in ms (default: 120000)", "120000").option("--json", "Output as JSON").option("--debug", "Keep sandbox directories for inspection").option("--model <model>", "Model to use for eval (e.g., sonnet, haiku, opus)").action(async (opts) => {
2019
2212
  printBanner();
2213
+ const hasFlags = opts.suite || opts.model || opts.runs !== "3" || opts.json || opts.debug;
2214
+ if (!hasFlags) {
2215
+ opts.suite = await select({
2216
+ message: "Suite",
2217
+ choices: [
2218
+ { name: "security (6 scenarios)", value: "security" },
2219
+ { name: "conventions (5 scenarios)", value: "conventions" },
2220
+ { name: "workflow (2 scenarios)", value: "workflow" },
2221
+ { name: "all (13 scenarios)", value: void 0 }
2222
+ ]
2223
+ });
2224
+ opts.runs = await select({
2225
+ message: "Runs per scenario",
2226
+ choices: [
2227
+ { name: "1 \u2014 fast", value: "1" },
2228
+ { name: "3 \u2014 default", value: "3" },
2229
+ { name: "5 \u2014 thorough", value: "5" }
2230
+ ]
2231
+ });
2232
+ opts.model = await select({
2233
+ message: "Model",
2234
+ choices: [
2235
+ { name: "haiku \u2014 cheapest", value: "haiku" },
2236
+ { name: "sonnet \u2014 balanced", value: "sonnet" },
2237
+ { name: "opus \u2014 best", value: "opus" }
2238
+ ]
2239
+ });
2240
+ log.blank();
2241
+ }
2020
2242
  const claudeAvailable = await checkClaudeCli();
2021
2243
  if (!claudeAvailable) {
2022
2244
  log.error("Claude CLI not found. Install it: https://docs.anthropic.com/en/docs/claude-code");
@@ -2203,9 +2425,15 @@ Also review .claude/settings.json hooks:
2203
2425
  - Read the existing hooks in .claude/settings.json
2204
2426
  - If you see project-specific patterns that deserve hooks (e.g., protected directories, test file patterns, migration files), suggest adding them
2205
2427
  - If no PostCompact hook exists, suggest adding one that re-injects TASKS.md after context compaction (critical for session continuity)
2428
+ - If no SessionStart hook exists, suggest adding one that injects TASKS.md at session startup
2206
2429
  - DO NOT overwrite existing hooks \u2014 only add new ones that are specific to this project
2207
2430
  - Print hook suggestions at the end with the exact JSON to add, don't modify settings.json directly
2208
2431
 
2432
+ Also check for advanced configuration opportunities:
2433
+ - If the project has both app code and tests, suggest creating path-scoped .claude/rules/ files with paths: frontmatter (e.g., test conventions only load when editing test files)
2434
+ - If the project uses external APIs (Stripe, GitHub, AWS SDKs, etc.), suggest sandbox.network.allowedDomains to restrict outbound traffic
2435
+ - If you detect a monorepo (Turborepo, Lerna, pnpm workspaces, multiple package.json), suggest claudeMdExcludes in settings.json
2436
+
2209
2437
  Rules:
2210
2438
  - Don't remove existing content \u2014 only add or improve
2211
2439
  - Be specific to THIS project, not generic advice