claude-launchpad 0.4.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,7 +180,7 @@ 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
185
  | `workflow` | 2 | Git conventions, session continuity |
198
186
 
@@ -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,
@@ -421,10 +423,18 @@ function generateSettings(detected) {
421
423
  "Bash(rm -rf ~)",
422
424
  "Read(.env)",
423
425
  "Read(.env.*)",
424
- "Read(secrets/**)"
426
+ "Read(secrets/**)",
427
+ "Read(~/.ssh/*)",
428
+ "Read(~/.aws/*)",
429
+ "Read(~/.npmrc)"
425
430
  ]
426
431
  },
427
- hooks
432
+ hooks,
433
+ disableBypassPermissionsMode: "disable",
434
+ sandbox: {
435
+ enabled: true,
436
+ failIfUnavailable: true
437
+ }
428
438
  };
429
439
  }
430
440
  var SAFE_FORMATTERS = {
@@ -730,13 +740,14 @@ var RULES_DIR = "rules";
730
740
  async function parseClaudeConfig(projectRoot) {
731
741
  const root = resolve(projectRoot);
732
742
  const claudeDir = join3(root, CLAUDE_DIR);
733
- const [claudeMd, settings, hooks, rules, mcpServers, skills] = await Promise.all([
743
+ const [claudeMd, settings, hooks, rules, mcpServers, skills, claudeignore] = await Promise.all([
734
744
  readClaudeMd(root),
735
745
  readSettings(claudeDir),
736
746
  readHooks(claudeDir),
737
747
  readRules(claudeDir),
738
748
  readMcpServers(claudeDir),
739
- readSkills(claudeDir)
749
+ readSkills(claudeDir),
750
+ readFileOrNull(join3(root, ".claudeignore"))
740
751
  ]);
741
752
  const instructionCount = claudeMd ? countInstructions(claudeMd) : 0;
742
753
  return {
@@ -748,7 +759,9 @@ async function parseClaudeConfig(projectRoot) {
748
759
  hooks,
749
760
  rules,
750
761
  mcpServers,
751
- skills
762
+ skills,
763
+ claudeignorePath: claudeignore !== null ? join3(root, ".claudeignore") : null,
764
+ claudeignoreContent: claudeignore
752
765
  };
753
766
  }
754
767
  async function readClaudeMd(root) {
@@ -1088,10 +1101,63 @@ async function analyzeRules(config) {
1088
1101
  // src/commands/doctor/analyzers/permissions.ts
1089
1102
  async function analyzePermissions(config) {
1090
1103
  const issues = [];
1104
+ const settings = config.settings;
1105
+ const permissions = settings?.permissions;
1106
+ const denyList = permissions?.deny ?? [];
1107
+ const allowList = permissions?.allow ?? [];
1108
+ const credentialPatterns = ["Read(~/.ssh/*)", "Read(~/.aws/*)", "Read(~/.npmrc)"];
1109
+ const missingCreds = credentialPatterns.filter((p) => !denyList.includes(p));
1110
+ if (missingCreds.length > 0) {
1111
+ issues.push({
1112
+ analyzer: "Permissions",
1113
+ severity: "high",
1114
+ message: `Credential files not blocked: ${missingCreds.join(", ")} \u2014 Claude can read SSH keys, AWS creds, or npm tokens`,
1115
+ fix: "Add Read(~/.ssh/*), Read(~/.aws/*), Read(~/.npmrc) to permissions.deny"
1116
+ });
1117
+ }
1118
+ const hasBlanketBash = allowList.some((a) => a === "Bash" || a.startsWith("Bash") && !a.includes("("));
1119
+ if (hasBlanketBash) {
1120
+ issues.push({
1121
+ analyzer: "Permissions",
1122
+ severity: "high",
1123
+ message: "Bash is blanket-allowed without pattern restriction \u2014 all shell commands are auto-approved",
1124
+ fix: "Replace blanket Bash with scoped patterns like Bash(npm test) or remove it"
1125
+ });
1126
+ }
1127
+ if (settings?.disableBypassPermissionsMode !== "disable") {
1128
+ issues.push({
1129
+ analyzer: "Permissions",
1130
+ severity: "high",
1131
+ message: "Bypass permissions mode not disabled \u2014 --dangerously-skip-permissions bypasses all checks",
1132
+ fix: 'Add "disableBypassPermissionsMode": "disable" to settings.json'
1133
+ });
1134
+ }
1135
+ const sandbox = settings?.sandbox;
1136
+ if (sandbox?.enabled !== true) {
1137
+ issues.push({
1138
+ analyzer: "Permissions",
1139
+ severity: "medium",
1140
+ message: "Sandbox not enabled \u2014 hooks block tool calls but not subprocess access (e.g. cat .env via Bash)",
1141
+ fix: 'Add "sandbox": { "enabled": true, "failIfUnavailable": true } to settings.json'
1142
+ });
1143
+ }
1144
+ const hasEnvHook = config.hooks.some((h) => h.command?.includes(".env"));
1145
+ if (hasEnvHook && config.claudeignoreContent !== null) {
1146
+ const lines = config.claudeignoreContent.split("\n").map((l) => l.trim());
1147
+ const hasEnvInIgnore = lines.some((l) => l === ".env" || l === ".env.*" || l === ".env*");
1148
+ if (!hasEnvInIgnore) {
1149
+ issues.push({
1150
+ analyzer: "Permissions",
1151
+ severity: "medium",
1152
+ message: ".env is protected by hooks but not in .claudeignore \u2014 cat .env via Bash bypasses hooks",
1153
+ fix: "Add .env to .claudeignore for defense in depth"
1154
+ });
1155
+ }
1156
+ }
1091
1157
  const hasBashSecurity = config.hooks.some(
1092
1158
  (h) => h.event === "PreToolUse" && (h.matcher?.includes("Bash") || !h.matcher)
1093
1159
  );
1094
- const bashAllowed = config.settings?.allowedTools;
1160
+ const bashAllowed = settings?.allowedTools;
1095
1161
  const hasBashAutoAllow = bashAllowed?.some(
1096
1162
  (t) => typeof t === "string" && t.toLowerCase().includes("bash")
1097
1163
  );
@@ -1100,7 +1166,7 @@ async function analyzePermissions(config) {
1100
1166
  analyzer: "Permissions",
1101
1167
  severity: "high",
1102
1168
  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)"
1169
+ fix: "Add a PreToolUse hook for Bash that blocks destructive commands"
1104
1170
  });
1105
1171
  }
1106
1172
  const hasForceProtection = config.hooks.some(
@@ -1125,7 +1191,7 @@ async function analyzePermissions(config) {
1125
1191
  });
1126
1192
  }
1127
1193
  }
1128
- const score = Math.max(0, 100 - issues.length * 20);
1194
+ const score = Math.max(0, 100 - issues.length * 15);
1129
1195
  return { name: "Permissions", issues, score };
1130
1196
  }
1131
1197
 
@@ -1303,7 +1369,11 @@ var FIX_TABLE = [
1303
1369
  { analyzer: "Rules", match: "No .claude/rules/", fix: (root) => createStarterRules(root) },
1304
1370
  { 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
1371
  { analyzer: "Hooks", match: "PostCompact", fix: (root) => addPostCompactHook(root) },
1306
- { analyzer: "Permissions", match: "force-push", fix: (root) => addForcePushProtection(root) }
1372
+ { analyzer: "Permissions", match: "force-push", fix: (root) => addForcePushProtection(root) },
1373
+ { analyzer: "Permissions", match: "Credential files not blocked", fix: (root) => addCredentialDenyRules(root) },
1374
+ { analyzer: "Permissions", match: "Bypass permissions mode", fix: (root) => addBypassDisable(root) },
1375
+ { analyzer: "Permissions", match: "Sandbox not enabled", fix: (root) => addSandboxSettings(root) },
1376
+ { analyzer: "Permissions", match: ".env is protected by hooks but not in .claudeignore", fix: (root) => addEnvToClaudeignore(root) }
1307
1377
  ];
1308
1378
  async function tryFix(issue, root, detected) {
1309
1379
  const entry = FIX_TABLE.find(
@@ -1405,6 +1475,49 @@ async function addPostCompactHook(root) {
1405
1475
  log.success("Added PostCompact hook (re-injects TASKS.md after compaction)");
1406
1476
  return true;
1407
1477
  }
1478
+ async function addCredentialDenyRules(root) {
1479
+ const settings = await readSettingsJson(root);
1480
+ const permissions = settings.permissions ?? {};
1481
+ const deny = permissions.deny ?? [];
1482
+ const toAdd = ["Read(~/.ssh/*)", "Read(~/.aws/*)", "Read(~/.npmrc)"];
1483
+ const missing = toAdd.filter((p) => !deny.includes(p));
1484
+ if (missing.length === 0) return false;
1485
+ settings.permissions = { ...permissions, deny: [...deny, ...missing] };
1486
+ await writeSettingsJson(root, settings);
1487
+ log.success("Added credential deny rules (SSH, AWS, npm)");
1488
+ return true;
1489
+ }
1490
+ async function addBypassDisable(root) {
1491
+ const settings = await readSettingsJson(root);
1492
+ if (settings.disableBypassPermissionsMode === "disable") return false;
1493
+ settings.disableBypassPermissionsMode = "disable";
1494
+ await writeSettingsJson(root, settings);
1495
+ log.success("Added disableBypassPermissionsMode: disable");
1496
+ return true;
1497
+ }
1498
+ async function addSandboxSettings(root) {
1499
+ const settings = await readSettingsJson(root);
1500
+ const sandbox = settings.sandbox;
1501
+ if (sandbox?.enabled === true) return false;
1502
+ settings.sandbox = { enabled: true, failIfUnavailable: true };
1503
+ await writeSettingsJson(root, settings);
1504
+ log.success("Enabled sandbox with failIfUnavailable");
1505
+ return true;
1506
+ }
1507
+ async function addEnvToClaudeignore(root) {
1508
+ const ignorePath = join5(root, ".claudeignore");
1509
+ let content;
1510
+ try {
1511
+ content = await readFile4(ignorePath, "utf-8");
1512
+ } catch {
1513
+ return false;
1514
+ }
1515
+ const lines = content.split("\n").map((l) => l.trim());
1516
+ if (lines.some((l) => l === ".env" || l === ".env.*" || l === ".env*")) return false;
1517
+ await writeFile2(ignorePath, content.trimEnd() + "\n.env\n.env.*\n");
1518
+ log.success("Added .env to .claudeignore");
1519
+ return true;
1520
+ }
1408
1521
  async function addClaudeMdSection(root, heading, content) {
1409
1522
  const claudeMdPath = join5(root, "CLAUDE.md");
1410
1523
  let existing;
@@ -1608,6 +1721,7 @@ function createDoctorCommand() {
1608
1721
 
1609
1722
  // src/commands/eval/index.ts
1610
1723
  import { Command as Command3 } from "commander";
1724
+ import { select } from "@inquirer/prompts";
1611
1725
  import ora from "ora";
1612
1726
  import chalk2 from "chalk";
1613
1727
  import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
@@ -2017,6 +2131,35 @@ async function listAllFiles(dir) {
2017
2131
  function createEvalCommand() {
2018
2132
  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
2133
  printBanner();
2134
+ const hasFlags = opts.suite || opts.model || opts.runs !== "3" || opts.json || opts.debug;
2135
+ if (!hasFlags) {
2136
+ opts.suite = await select({
2137
+ message: "Suite",
2138
+ choices: [
2139
+ { name: "security (6 scenarios)", value: "security" },
2140
+ { name: "conventions (5 scenarios)", value: "conventions" },
2141
+ { name: "workflow (2 scenarios)", value: "workflow" },
2142
+ { name: "all (13 scenarios)", value: void 0 }
2143
+ ]
2144
+ });
2145
+ opts.runs = await select({
2146
+ message: "Runs per scenario",
2147
+ choices: [
2148
+ { name: "1 \u2014 fast", value: "1" },
2149
+ { name: "3 \u2014 default", value: "3" },
2150
+ { name: "5 \u2014 thorough", value: "5" }
2151
+ ]
2152
+ });
2153
+ opts.model = await select({
2154
+ message: "Model",
2155
+ choices: [
2156
+ { name: "haiku \u2014 cheapest", value: "haiku" },
2157
+ { name: "sonnet \u2014 balanced", value: "sonnet" },
2158
+ { name: "opus \u2014 best", value: "opus" }
2159
+ ]
2160
+ });
2161
+ log.blank();
2162
+ }
2020
2163
  const claudeAvailable = await checkClaudeCli();
2021
2164
  if (!claudeAvailable) {
2022
2165
  log.error("Claude CLI not found. Install it: https://docs.anthropic.com/en/docs/claude-code");