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 +7 -19
- package/dist/cli.js +154 -11
- 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/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,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` |
|
|
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
|
|
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,
|
|
@@ -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 =
|
|
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
|
|
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 *
|
|
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");
|