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 +8 -20
- package/dist/cli.js +243 -15
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/scenarios/security/credential-read.yaml +35 -0
- package/scenarios/security/sandbox-escape.yaml +39 -0
- package/scenarios/workflow/deferred-tracking.yaml +58 -0
- package/scenarios/workflow/memory-persistence.yaml +49 -0
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
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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` |
|
|
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` |
|
|
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
|
|
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,
|
|
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"))
|
|
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
|
|
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 =
|
|
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
|
|
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 *
|
|
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).
|
|
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
|