claude-code-arcane 2.16.0 → 2.18.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/CHANGELOG.md +19 -0
- package/README.md +15 -3
- package/agents/quality/security-architect.md +1 -1
- package/dist/cli.js +306 -12
- package/docs/SKILLS-CATALOG.md +3 -3
- package/docs/USER-GUIDE.md +18 -1
- package/package.json +1 -1
- package/profiles/backend-go.yaml +0 -1
- package/profiles/security.yaml +0 -1
- package/profiles/unity-dev.yaml +1 -1
- package/profiles/unreal-dev.yaml +1 -1
- package/skills/arcane-add/SKILL.md +1 -1
- package/skills/{security-audit → game-security-audit}/SKILL.md +12 -16
- package/skills/ue-networking-replication/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
# [2.18.0](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.17.0...v2.18.0) (2026-10-17)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **skills:** rename security-audit to game-security-audit, games only ([e01d48e](https://github.com/SebastianLuser/claude-code-arcane/commit/e01d48e8c17c5393c7b8fe5672b02b50dc732f16))
|
|
7
|
+
|
|
8
|
+
# [2.17.0](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.16.0...v2.17.0) (2026-10-16)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **update:** narrow the old Bash(gh *) allow in existing installs ([4b033be](https://github.com/SebastianLuser/claude-code-arcane/commit/4b033befa1c3f68e84855cbf9e430f21be836293))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **cli:** add arcane doctor to check that an install works ([cd36de9](https://github.com/SebastianLuser/claude-code-arcane/commit/cd36de93baad06f6e5e47151eb2902fc6d6d1651))
|
|
19
|
+
|
|
1
20
|
# [2.16.0](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.15.0...v2.16.0) (2026-10-16)
|
|
2
21
|
|
|
3
22
|
|
package/README.md
CHANGED
|
@@ -114,7 +114,7 @@ npx claude-code-arcane install unity-dev --dry-run # Preview without installin
|
|
|
114
114
|
```bash
|
|
115
115
|
npx claude-code-arcane add # Interactive picker (same wizard as install)
|
|
116
116
|
npx claude-code-arcane add api-design # Add a single skill
|
|
117
|
-
npx claude-code-arcane add
|
|
117
|
+
npx claude-code-arcane add owasp-top10-check secret-management # Add multiple skills
|
|
118
118
|
npx claude-code-arcane add +security # Add an entire profile
|
|
119
119
|
npx claude-code-arcane add +database +testing # Add multiple profiles
|
|
120
120
|
```
|
|
@@ -149,6 +149,7 @@ Installs worktree-isolation scripts into `~/.claude/scripts/` and adds a `Sessio
|
|
|
149
149
|
```bash
|
|
150
150
|
npx claude-code-arcane list # List all profiles and skills
|
|
151
151
|
npx claude-code-arcane status # Show current installation
|
|
152
|
+
npx claude-code-arcane doctor # Check that hooks, settings and content work
|
|
152
153
|
npx claude-code-arcane update # Check for updates
|
|
153
154
|
npx claude-code-arcane clean --force # Remove Arcane entirely from project
|
|
154
155
|
```
|
|
@@ -191,7 +192,7 @@ Older names still install and update: `backend-ts`, `frontend`, `mobile`, `flutt
|
|
|
191
192
|
| Profile | Stack | Skills | Agents |
|
|
192
193
|
|---------|-------|--------|--------|
|
|
193
194
|
| `backend-typescript` | Backend TypeScript — Fastify, Prisma, Zod, API design | 27 | engineering |
|
|
194
|
-
| `backend-go` | Backend Go — Clean Arch, DB, auth, API, CI |
|
|
195
|
+
| `backend-go` | Backend Go — Clean Arch, DB, auth, API, CI | 25 | engineering |
|
|
195
196
|
| `backend-dotnet` | Backend .NET — ASP.NET Core, Vertical Slice/Clean, EF Core, JWT | 27 | engineering |
|
|
196
197
|
| `backend-nestjs` | Backend NestJS — feature modules, DI, Prisma, JWT, microservices | 27 | engineering |
|
|
197
198
|
| `backend-nextjs` | Next.js full-stack — App Router, RSC, Server Actions, SEO, AI SDK | 25 | engineering |
|
|
@@ -230,7 +231,7 @@ Older names still install and update: `backend-ts`, `frontend`, `mobile`, `flutt
|
|
|
230
231
|
|---------|------|--------|--------|
|
|
231
232
|
| `infra` | Terraform, observability, secrets, rollback, K8s | 26 | devops |
|
|
232
233
|
| `database` | Indexing, migrations, seeding, replicas, optimization | 8 | — |
|
|
233
|
-
| `security` | Audits, OWASP, secrets, backups |
|
|
234
|
+
| `security` | Audits, OWASP, secrets, backups | 9 | — |
|
|
234
235
|
| `testing` | Contract, performance, regression, flakiness | 11 | quality |
|
|
235
236
|
| `ai` | LLM cost optimization, RAG, ML, data engineering, agent and MCP design | 10 | ai |
|
|
236
237
|
|
|
@@ -310,6 +311,17 @@ It ships the in-editor skills that operate and extend the MCP (`unreal-mcp`, `cr
|
|
|
310
311
|
|
|
311
312
|
Building your own server? `/mcp-server-builder` scaffolds one.
|
|
312
313
|
|
|
314
|
+
### Recommended complements
|
|
315
|
+
|
|
316
|
+
Arcane leaves out what these already do well. Install them once, at user level, next to any profile:
|
|
317
|
+
|
|
318
|
+
| Complement | What it adds | Install |
|
|
319
|
+
|------------|--------------|---------|
|
|
320
|
+
| [Cloudflare `security-audit`](https://github.com/cloudflare/security-audit-skill) | Security audit for any codebase. Arcane's `/game-security-audit` only covers games (saves, cheats, netcode) | `npx skills add https://github.com/cloudflare/security-audit-skill --skill security-audit --global` |
|
|
321
|
+
| [ponytail](https://github.com/DietrichGebert/ponytail) | Plugin that makes Claude write the least code that fully solves the task | `/plugin marketplace add DietrichGebert/ponytail`, then `/plugin install ponytail@ponytail` |
|
|
322
|
+
|
|
323
|
+
Arcane used to ship its own `security-audit`, so a project installed before the rename would end up with two skills under that name. Run `npx claude-code-arcane update` there first: it moves the old one to `.claude/.arcane-trash/`.
|
|
324
|
+
|
|
313
325
|
---
|
|
314
326
|
|
|
315
327
|
## Examples by Project
|
|
@@ -6,7 +6,7 @@ permissionMode: acceptEdits
|
|
|
6
6
|
model: sonnet
|
|
7
7
|
maxTurns: 20
|
|
8
8
|
memory: project
|
|
9
|
-
skills: [security-
|
|
9
|
+
skills: [security-pen-testing, soc2-compliance]
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
Sos el **Security Architect**. Owner de que el sistema sea defensible.
|
package/dist/cli.js
CHANGED
|
@@ -2940,6 +2940,13 @@ function syncUnhashedFiles(root, claudeDir, merged, dryRun) {
|
|
|
2940
2940
|
dirty = true;
|
|
2941
2941
|
}
|
|
2942
2942
|
}
|
|
2943
|
+
const allow = settings.permissions?.allow;
|
|
2944
|
+
if (Array.isArray(allow) && allow.includes("Bash(gh *)")) {
|
|
2945
|
+
const narrowed = merged.permissions.allow.filter((r) => r.startsWith("Bash(gh ") && !allow.includes(r));
|
|
2946
|
+
settings.permissions.allow = allow.flatMap((r) => r === "Bash(gh *)" ? narrowed : [r]);
|
|
2947
|
+
changed.push("settings.json (Bash(gh *) narrowed)");
|
|
2948
|
+
dirty = true;
|
|
2949
|
+
}
|
|
2943
2950
|
if (dirty && !dryRun) {
|
|
2944
2951
|
fs15.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
2945
2952
|
`, "utf-8");
|
|
@@ -3368,12 +3375,296 @@ function removeHookFromSettings() {
|
|
|
3368
3375
|
writeSettings(settings);
|
|
3369
3376
|
}
|
|
3370
3377
|
|
|
3371
|
-
// src/
|
|
3378
|
+
// src/commands/doctor.ts
|
|
3372
3379
|
import fs18 from "fs";
|
|
3373
3380
|
import path21 from "path";
|
|
3381
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
3374
3382
|
import chalk11 from "chalk";
|
|
3383
|
+
var DANGEROUS_ALLOWS = /* @__PURE__ */ new Set([
|
|
3384
|
+
"Bash",
|
|
3385
|
+
"Bash(*)",
|
|
3386
|
+
"Bash(gh *)",
|
|
3387
|
+
"Bash(git *)",
|
|
3388
|
+
"Bash(git push*)",
|
|
3389
|
+
"Bash(git reset*)",
|
|
3390
|
+
"Bash(rm *)",
|
|
3391
|
+
"Bash(sudo *)",
|
|
3392
|
+
"Bash(bash *)",
|
|
3393
|
+
"Bash(sh *)",
|
|
3394
|
+
"Bash(eval *)"
|
|
3395
|
+
]);
|
|
3396
|
+
var SECRET_PATTERNS = [
|
|
3397
|
+
["Anthropic key", /sk-ant-[A-Za-z0-9_-]{20,}/],
|
|
3398
|
+
["OpenAI key", /sk-(?:proj-)?[A-Za-z0-9]{32,}/],
|
|
3399
|
+
["GitHub token", /\b(?:gh[pousr]_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{22,})/],
|
|
3400
|
+
["Slack token", /\bxox[abpr]-[A-Za-z0-9-]{10,}/],
|
|
3401
|
+
["AWS access key", /\bAKIA[0-9A-Z]{16}\b/],
|
|
3402
|
+
["Google API key", /\bAIza[0-9A-Za-z_-]{35}/],
|
|
3403
|
+
["Stripe live key", /\b[sr]k_live_[0-9A-Za-z]{20,}/],
|
|
3404
|
+
["private key", /-----BEGIN [A-Z ]*PRIVATE KEY-----/]
|
|
3405
|
+
];
|
|
3406
|
+
var SECRET_KEY = /(?:TOKEN|SECRET|PASSWORD|PASSWD|API_?KEY|ACCESS_?KEY)$/i;
|
|
3407
|
+
var defaultProbes = {
|
|
3408
|
+
findBash,
|
|
3409
|
+
checkSyntax(bash, file) {
|
|
3410
|
+
const result = spawnSync2(bash, ["-n", file.replace(/\\/g, "/")], { encoding: "utf-8" });
|
|
3411
|
+
if (result.error) return result.error.message;
|
|
3412
|
+
return result.status === 0 ? null : (result.stderr || "syntax error").trim();
|
|
3413
|
+
},
|
|
3414
|
+
findPython() {
|
|
3415
|
+
for (const py of ["python3", "python", "py"]) {
|
|
3416
|
+
const result = spawnSync2(py, ["--version"], { encoding: "utf-8" });
|
|
3417
|
+
if (!result.error && result.status === 0) return py;
|
|
3418
|
+
}
|
|
3419
|
+
return null;
|
|
3420
|
+
}
|
|
3421
|
+
};
|
|
3422
|
+
function findBash() {
|
|
3423
|
+
if (process.platform !== "win32") {
|
|
3424
|
+
return spawnSync2("bash", ["--version"]).error ? null : "bash";
|
|
3425
|
+
}
|
|
3426
|
+
const override = process.env.CLAUDE_CODE_GIT_BASH_PATH;
|
|
3427
|
+
if (override && fs18.existsSync(override)) return override;
|
|
3428
|
+
const systemRoot = (process.env.SystemRoot ?? "C:\\Windows").toLowerCase();
|
|
3429
|
+
for (const dir of (process.env.PATH ?? "").split(path21.delimiter)) {
|
|
3430
|
+
const lower = dir.toLowerCase();
|
|
3431
|
+
if (!dir || lower.startsWith(systemRoot) || lower.includes("windowsapps")) continue;
|
|
3432
|
+
const candidate = path21.join(dir, "bash.exe");
|
|
3433
|
+
if (fs18.existsSync(candidate)) return candidate;
|
|
3434
|
+
}
|
|
3435
|
+
return null;
|
|
3436
|
+
}
|
|
3437
|
+
function readJson(file) {
|
|
3438
|
+
try {
|
|
3439
|
+
return { value: JSON.parse(fs18.readFileSync(file, "utf-8")) };
|
|
3440
|
+
} catch (error) {
|
|
3441
|
+
return { error: error.message };
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
function commandsOf(settings) {
|
|
3445
|
+
const commands = [];
|
|
3446
|
+
const hooks = settings.hooks ?? {};
|
|
3447
|
+
for (const groups of Object.values(hooks)) {
|
|
3448
|
+
for (const group of Array.isArray(groups) ? groups : []) {
|
|
3449
|
+
for (const entry of group.hooks ?? []) {
|
|
3450
|
+
if (typeof entry.command === "string") commands.push(entry.command);
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
const status = settings.statusLine;
|
|
3455
|
+
if (typeof status?.command === "string") commands.push(status.command);
|
|
3456
|
+
return commands;
|
|
3457
|
+
}
|
|
3458
|
+
function scriptsOf(command) {
|
|
3459
|
+
const scripts = [...command.matchAll(/\.claude\/((?:hooks\/)?[\w.-]+\.(?:sh|py))/g)].map((m) => m[1]);
|
|
3460
|
+
const python = command.match(/run-py\.sh"?\s+([\w.-]+\.py)/);
|
|
3461
|
+
if (python) scripts.push(`hooks/${python[1]}`);
|
|
3462
|
+
return scripts;
|
|
3463
|
+
}
|
|
3464
|
+
function checkSettings(claudeDir, cli) {
|
|
3465
|
+
const file = path21.join(claudeDir, "settings.json");
|
|
3466
|
+
if (!fs18.existsSync(file)) {
|
|
3467
|
+
return {
|
|
3468
|
+
check: { name: "settings.json", verdict: "fail", detail: "missing", fix: `Run \`${cli} update --here --force\`.` }
|
|
3469
|
+
};
|
|
3470
|
+
}
|
|
3471
|
+
const { value, error } = readJson(file);
|
|
3472
|
+
if (error || typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
3473
|
+
return {
|
|
3474
|
+
check: {
|
|
3475
|
+
name: "settings.json",
|
|
3476
|
+
verdict: "fail",
|
|
3477
|
+
detail: `not valid JSON: ${error ?? "not an object"}`,
|
|
3478
|
+
fix: "Fix the syntax by hand, or restore it from .claude.bak/."
|
|
3479
|
+
}
|
|
3480
|
+
};
|
|
3481
|
+
}
|
|
3482
|
+
return { check: { name: "settings.json", verdict: "ok", detail: "valid JSON" }, settings: value };
|
|
3483
|
+
}
|
|
3484
|
+
function checkHooks(claudeDir, settings, probes, cli) {
|
|
3485
|
+
const commands = commandsOf(settings);
|
|
3486
|
+
const scripts = [...new Set(commands.flatMap(scriptsOf))];
|
|
3487
|
+
const checks = [];
|
|
3488
|
+
const missing = scripts.filter((s) => !fs18.existsSync(path21.join(claudeDir, s)));
|
|
3489
|
+
checks.push(
|
|
3490
|
+
missing.length ? {
|
|
3491
|
+
name: "hook scripts",
|
|
3492
|
+
verdict: "fail",
|
|
3493
|
+
detail: `settings.json runs files that are not there: ${missing.join(", ")}`,
|
|
3494
|
+
fix: `If Arcane ships them, \`${cli} update --here\` restores them; otherwise remove those hooks from settings.json.`
|
|
3495
|
+
} : { name: "hook scripts", verdict: "ok", detail: `${scripts.length} scripts found` }
|
|
3496
|
+
);
|
|
3497
|
+
const relative = commands.filter((c) => /^bash \.claude\/hooks\//.test(c));
|
|
3498
|
+
if (relative.length) {
|
|
3499
|
+
checks.push({
|
|
3500
|
+
name: "hook paths",
|
|
3501
|
+
verdict: "warn",
|
|
3502
|
+
detail: `${relative.length} hooks use a relative path, which fails inside a worktree`,
|
|
3503
|
+
fix: `Run \`${cli} update --here\`: it rewrites them with $CLAUDE_PROJECT_DIR.`
|
|
3504
|
+
});
|
|
3505
|
+
}
|
|
3506
|
+
const bash = probes.findBash();
|
|
3507
|
+
if (!bash) {
|
|
3508
|
+
checks.push({
|
|
3509
|
+
name: "bash",
|
|
3510
|
+
verdict: "fail",
|
|
3511
|
+
detail: "not found: no hook can run",
|
|
3512
|
+
fix: process.platform === "win32" ? "Install Git for Windows, or set CLAUDE_CODE_GIT_BASH_PATH." : "Install bash."
|
|
3513
|
+
});
|
|
3514
|
+
return checks;
|
|
3515
|
+
}
|
|
3516
|
+
const broken = scripts.filter((s) => s.endsWith(".sh") && !missing.includes(s)).map((s) => [s, probes.checkSyntax(bash, path21.join(claudeDir, s))]).filter(([, error]) => error);
|
|
3517
|
+
checks.push(
|
|
3518
|
+
broken.length ? {
|
|
3519
|
+
name: "hook syntax",
|
|
3520
|
+
verdict: "fail",
|
|
3521
|
+
detail: broken.map(([s, error]) => `${s}: ${error}`).join("; "),
|
|
3522
|
+
fix: `Run \`${cli} update --here --force\` to restore the shipped version.`
|
|
3523
|
+
} : { name: "hook syntax", verdict: "ok", detail: `bash parses every script (${bash})` }
|
|
3524
|
+
);
|
|
3525
|
+
return checks;
|
|
3526
|
+
}
|
|
3527
|
+
function checkPython(probes) {
|
|
3528
|
+
const python = probes.findPython();
|
|
3529
|
+
return python ? { name: "python", verdict: "ok", detail: `${python} starts` } : {
|
|
3530
|
+
name: "python",
|
|
3531
|
+
verdict: "warn",
|
|
3532
|
+
detail: "no working Python: the guard and the session watch do nothing",
|
|
3533
|
+
fix: "Install Python 3.9+ and make sure `python --version` works in Git Bash."
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
function checkContent(claudeDir, manifest, cli) {
|
|
3537
|
+
const missing = [
|
|
3538
|
+
...(manifest.installed_skills ?? []).filter((s) => !fs18.existsSync(path21.join(claudeDir, "skills", s, "SKILL.md"))).map((s) => `skill ${s}`),
|
|
3539
|
+
...(manifest.installed_rules ?? []).filter((r) => !fs18.existsSync(path21.join(claudeDir, "rules", `${r}.md`))).map((r) => `rule ${r}`)
|
|
3540
|
+
];
|
|
3541
|
+
if (!missing.length) {
|
|
3542
|
+
return { name: "installed content", verdict: "ok", detail: "every skill and rule in the manifest is on disk" };
|
|
3543
|
+
}
|
|
3544
|
+
const shown = missing.slice(0, 5).join(", ") + (missing.length > 5 ? ` and ${missing.length - 5} more` : "");
|
|
3545
|
+
return {
|
|
3546
|
+
name: "installed content",
|
|
3547
|
+
verdict: "fail",
|
|
3548
|
+
detail: `missing from disk: ${shown}`,
|
|
3549
|
+
fix: `Run \`${cli} update --here\` to restore them.`
|
|
3550
|
+
};
|
|
3551
|
+
}
|
|
3552
|
+
function checkPermissions(claudeDir, settings, cli) {
|
|
3553
|
+
const found = [];
|
|
3554
|
+
for (const [file, data] of [
|
|
3555
|
+
["settings.json", settings],
|
|
3556
|
+
["settings.local.json", readJson(path21.join(claudeDir, "settings.local.json")).value]
|
|
3557
|
+
]) {
|
|
3558
|
+
const permissions = data?.permissions;
|
|
3559
|
+
const allow = Array.isArray(permissions?.allow) ? permissions.allow : [];
|
|
3560
|
+
for (const rule of allow) {
|
|
3561
|
+
if (typeof rule === "string" && DANGEROUS_ALLOWS.has(rule.trim())) found.push(`${file}: ${rule}`);
|
|
3562
|
+
}
|
|
3563
|
+
if (permissions?.defaultMode === "bypassPermissions") found.push(`${file}: defaultMode bypassPermissions`);
|
|
3564
|
+
}
|
|
3565
|
+
return found.length ? {
|
|
3566
|
+
name: "permissions",
|
|
3567
|
+
verdict: "warn",
|
|
3568
|
+
detail: `rules that skip every confirmation: ${found.join(", ")}`,
|
|
3569
|
+
fix: found.some((f) => f.endsWith("Bash(gh *)")) ? `Run \`${cli} update --here\` to narrow the old \`Bash(gh *)\`; narrow any other by hand, unless you mean it.` : "Narrow them by hand (e.g. `Bash(npm test*)` instead of `Bash(*)`), unless you mean it."
|
|
3570
|
+
} : { name: "permissions", verdict: "ok", detail: "no allow rule covers a whole tool or a destructive command" };
|
|
3571
|
+
}
|
|
3572
|
+
function* strings(value, key = "") {
|
|
3573
|
+
if (typeof value === "string") yield [key, value];
|
|
3574
|
+
else if (Array.isArray(value)) for (const item of value) yield* strings(item, key);
|
|
3575
|
+
else if (value && typeof value === "object") {
|
|
3576
|
+
for (const [k, v] of Object.entries(value)) yield* strings(v, k);
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
function secretsIn(value) {
|
|
3580
|
+
const found = /* @__PURE__ */ new Set();
|
|
3581
|
+
for (const [key, text] of strings(value)) {
|
|
3582
|
+
for (const [label, pattern] of SECRET_PATTERNS) if (pattern.test(text)) found.add(label);
|
|
3583
|
+
if (SECRET_KEY.test(key) && text.length >= 8 && !/\$\{?[A-Za-z_]/.test(text)) found.add(`literal ${key}`);
|
|
3584
|
+
}
|
|
3585
|
+
return [...found];
|
|
3586
|
+
}
|
|
3587
|
+
function checkSecrets(target, claudeDir) {
|
|
3588
|
+
const committed = [];
|
|
3589
|
+
const local = [];
|
|
3590
|
+
for (const [file, bucket] of [
|
|
3591
|
+
[path21.join(claudeDir, "settings.json"), committed],
|
|
3592
|
+
[path21.join(target, ".mcp.json"), committed],
|
|
3593
|
+
[path21.join(claudeDir, "settings.local.json"), local]
|
|
3594
|
+
]) {
|
|
3595
|
+
const { value } = readJson(file);
|
|
3596
|
+
for (const label of secretsIn(value)) bucket.push(`${path21.relative(target, file).replace(/\\/g, "/")} (${label})`);
|
|
3597
|
+
}
|
|
3598
|
+
if (committed.length) {
|
|
3599
|
+
return {
|
|
3600
|
+
name: "secrets",
|
|
3601
|
+
verdict: "fail",
|
|
3602
|
+
detail: `credentials in files that get committed: ${committed.join(", ")}`,
|
|
3603
|
+
fix: "Move them to environment variables and reference them as ${VAR}; rotate any that were pushed."
|
|
3604
|
+
};
|
|
3605
|
+
}
|
|
3606
|
+
if (local.length) {
|
|
3607
|
+
return {
|
|
3608
|
+
name: "secrets",
|
|
3609
|
+
verdict: "warn",
|
|
3610
|
+
detail: `credentials in settings.local.json: ${local.join(", ")}`,
|
|
3611
|
+
fix: "It is not committed, but an environment variable keeps it out of backups and shares."
|
|
3612
|
+
};
|
|
3613
|
+
}
|
|
3614
|
+
return { name: "secrets", verdict: "ok", detail: "no credentials found in settings or .mcp.json" };
|
|
3615
|
+
}
|
|
3616
|
+
function diagnose(target, probes = defaultProbes) {
|
|
3617
|
+
const claudeDir = path21.join(target, ".claude");
|
|
3618
|
+
const cli = cliInvocation();
|
|
3619
|
+
const manifest = readManifest(target);
|
|
3620
|
+
if (!manifest) {
|
|
3621
|
+
return [
|
|
3622
|
+
{
|
|
3623
|
+
name: "installation",
|
|
3624
|
+
verdict: "fail",
|
|
3625
|
+
detail: "no .claude/arcane-manifest.json",
|
|
3626
|
+
fix: `Run \`${cli} install <profile>\`.`
|
|
3627
|
+
}
|
|
3628
|
+
];
|
|
3629
|
+
}
|
|
3630
|
+
const checks = [
|
|
3631
|
+
{ name: "installation", verdict: "ok", detail: `Arcane ${manifest.arcane_version}, ${manifest.profiles.join(" + ")}` }
|
|
3632
|
+
];
|
|
3633
|
+
const { check, settings } = checkSettings(claudeDir, cli);
|
|
3634
|
+
checks.push(check);
|
|
3635
|
+
if (settings) checks.push(...checkHooks(claudeDir, settings, probes, cli));
|
|
3636
|
+
checks.push(checkPython(probes), checkContent(claudeDir, manifest, cli));
|
|
3637
|
+
if (settings) checks.push(checkPermissions(claudeDir, settings, cli));
|
|
3638
|
+
checks.push(checkSecrets(target, claudeDir));
|
|
3639
|
+
return checks;
|
|
3640
|
+
}
|
|
3641
|
+
var ICONS = { ok: chalk11.green("\u2713"), warn: chalk11.yellow("!"), fail: chalk11.red("\u2717") };
|
|
3642
|
+
async function doctorCommand(opts = {}) {
|
|
3643
|
+
const target = path21.resolve(opts.target ?? process.cwd());
|
|
3644
|
+
const checks = diagnose(target);
|
|
3645
|
+
console.log(chalk11.bold(`
|
|
3646
|
+
=== Arcane Doctor: ${target} ===
|
|
3647
|
+
`));
|
|
3648
|
+
for (const check of checks) {
|
|
3649
|
+
console.log(` ${ICONS[check.verdict]} ${chalk11.bold(check.name)}: ${check.detail}`);
|
|
3650
|
+
if (check.fix && check.verdict !== "ok") console.log(chalk11.dim(` fix: ${check.fix}`));
|
|
3651
|
+
}
|
|
3652
|
+
const fails = checks.filter((c) => c.verdict === "fail").length;
|
|
3653
|
+
const warns = checks.filter((c) => c.verdict === "warn").length;
|
|
3654
|
+
console.log(
|
|
3655
|
+
`
|
|
3656
|
+
${fails ? chalk11.red(`${fails} failed`) : chalk11.green("nothing failed")}${warns ? chalk11.yellow(`, ${warns} warnings`) : ""}
|
|
3657
|
+
`
|
|
3658
|
+
);
|
|
3659
|
+
if (fails) process.exitCode = 1;
|
|
3660
|
+
}
|
|
3661
|
+
|
|
3662
|
+
// src/update-check.ts
|
|
3663
|
+
import fs19 from "fs";
|
|
3664
|
+
import path22 from "path";
|
|
3665
|
+
import chalk12 from "chalk";
|
|
3375
3666
|
function checkFile() {
|
|
3376
|
-
return
|
|
3667
|
+
return path22.join(arcaneHome(), "last-check.json");
|
|
3377
3668
|
}
|
|
3378
3669
|
var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
|
|
3379
3670
|
var GITHUB_OWNER2 = "SebastianLuser";
|
|
@@ -3443,23 +3734,23 @@ async function checkForUpdates(opts = {}) {
|
|
|
3443
3734
|
function printUpdateNotice(result) {
|
|
3444
3735
|
if (result.cli_update_available) {
|
|
3445
3736
|
console.log(
|
|
3446
|
-
|
|
3737
|
+
chalk12.yellow(
|
|
3447
3738
|
`
|
|
3448
3739
|
Arcane CLI update: ${result.cli_version} \u2192 ${result.latest_cli_version}`
|
|
3449
3740
|
)
|
|
3450
3741
|
);
|
|
3451
3742
|
const how = isGloballyInstalled() ? ` Run: npm install -g ${PACKAGE_NAME3}@latest` : ` You are on an older CLI. Latest: ${PACKAGE_NAME3}@${result.latest_cli_version}`;
|
|
3452
|
-
console.log(
|
|
3743
|
+
console.log(chalk12.dim(how));
|
|
3453
3744
|
}
|
|
3454
3745
|
if (result.content_update_available ?? true) {
|
|
3455
3746
|
console.log(
|
|
3456
|
-
|
|
3747
|
+
chalk12.yellow(
|
|
3457
3748
|
`
|
|
3458
3749
|
Arcane content update: ${result.local_version} \u2192 ${result.remote_sha}`
|
|
3459
3750
|
)
|
|
3460
3751
|
);
|
|
3461
3752
|
console.log(
|
|
3462
|
-
|
|
3753
|
+
chalk12.dim(` Run: ${cliInvocation()} update (--dry-run to preview)`)
|
|
3463
3754
|
);
|
|
3464
3755
|
}
|
|
3465
3756
|
console.log("");
|
|
@@ -3502,8 +3793,8 @@ async function getLatestNpmVersion() {
|
|
|
3502
3793
|
function readCachedCheck() {
|
|
3503
3794
|
try {
|
|
3504
3795
|
const file = checkFile();
|
|
3505
|
-
if (!
|
|
3506
|
-
return JSON.parse(
|
|
3796
|
+
if (!fs19.existsSync(file)) return null;
|
|
3797
|
+
return JSON.parse(fs19.readFileSync(file, "utf-8"));
|
|
3507
3798
|
} catch {
|
|
3508
3799
|
return null;
|
|
3509
3800
|
}
|
|
@@ -3511,11 +3802,11 @@ function readCachedCheck() {
|
|
|
3511
3802
|
function writeCachedCheck(result) {
|
|
3512
3803
|
try {
|
|
3513
3804
|
const file = checkFile();
|
|
3514
|
-
const dir =
|
|
3515
|
-
if (!
|
|
3516
|
-
|
|
3805
|
+
const dir = path22.dirname(file);
|
|
3806
|
+
if (!fs19.existsSync(dir)) {
|
|
3807
|
+
fs19.mkdirSync(dir, { recursive: true });
|
|
3517
3808
|
}
|
|
3518
|
-
|
|
3809
|
+
fs19.writeFileSync(file, JSON.stringify(result, null, 2) + "\n");
|
|
3519
3810
|
} catch {
|
|
3520
3811
|
}
|
|
3521
3812
|
}
|
|
@@ -3552,6 +3843,9 @@ program.command("list").description("List all available profiles and skills.").a
|
|
|
3552
3843
|
program.command("status").description("Show current Arcane installation status.").action(async () => {
|
|
3553
3844
|
await statusCommand();
|
|
3554
3845
|
});
|
|
3846
|
+
program.command("doctor").description("Check that this project's Arcane install works: hooks, settings, content, permissions, secrets.").option("-t, --target <dir>", "Project directory", process.cwd()).action(async (opts) => {
|
|
3847
|
+
await doctorCommand(opts);
|
|
3848
|
+
});
|
|
3555
3849
|
program.command("update").description(
|
|
3556
3850
|
"Update everything: the global npm package + all registered Arcane installations."
|
|
3557
3851
|
).option("-q, --quiet", "Quiet mode for hook usage").option("-n, --dry-run", "Show what would change without applying").option("-f, --force", "Overwrite even locally modified files").option("--here", "Update only the current repo (legacy per-repo behavior)").option("--no-self-update", "Skip updating the global npm package").option("--source <source>", "Content source: auto, github, or bundled", "auto").action(async (opts) => {
|
package/docs/SKILLS-CATALOG.md
CHANGED
|
@@ -72,7 +72,7 @@ Desde cualquier proyecto que tenga Arcane configurado, escribi `/` seguido del n
|
|
|
72
72
|
|
|
73
73
|
---
|
|
74
74
|
|
|
75
|
-
## Gamedev (
|
|
75
|
+
## Gamedev (26 skills)
|
|
76
76
|
|
|
77
77
|
| Skill | Descripcion | Uso |
|
|
78
78
|
|-------|-------------|-----|
|
|
@@ -84,6 +84,7 @@ Desde cualquier proyecto que tenga Arcane configurado, escribi `/` seguido del n
|
|
|
84
84
|
| `/consistency-check` | Detecta inconsistencias cross-documento en stats/items | `[full \| since-last \| entity:<name>]` |
|
|
85
85
|
| `/doc-gdd` | Genera Game Design Document completo | `[game-name]` |
|
|
86
86
|
| `/doc-pas` | Documentacion PAS (Problem-Analysis-Solution) | `[problem o decision title]` |
|
|
87
|
+
| `/game-security-audit` | Audit de seguridad para juegos Unity/Unreal: saves, red, cheats, input | `[full \| network \| save \| input \| quick]` |
|
|
87
88
|
| `/install-mcp` | Conecta Claude con el MCP del engine: Unity (package UPM) o Unreal (plugins del engine) | `[unity\|unreal] [--secondary] [path]` |
|
|
88
89
|
| `/meshy-derive` | Deriva targets (FBX, GLB web, blend, STL) desde el master GLB | `<slug> [game\|web\|edit\|print\|all]` |
|
|
89
90
|
| `/meshy-generate` | Genera asset 3D con Meshy: preview -> refine, con gate de creditos | `<slug> [--from-image <path>] [--spec <path>]` |
|
|
@@ -178,7 +179,7 @@ Instalables con el perfil `unreal-dev`. Procedencia y licencias en `ATTRIBUTION.
|
|
|
178
179
|
|
|
179
180
|
---
|
|
180
181
|
|
|
181
|
-
## General (
|
|
182
|
+
## General (53 skills)
|
|
182
183
|
|
|
183
184
|
| Skill | Descripcion | Uso |
|
|
184
185
|
|-------|-------------|-----|
|
|
@@ -224,7 +225,6 @@ Instalables con el perfil `unreal-dev`. Procedencia y licencias en `ATTRIBUTION.
|
|
|
224
225
|
| `/runbooks` | Crea y gestiona runbooks operativos | `[create <name> \| list \| update <name>]` |
|
|
225
226
|
| `/scope-check` | Analiza scope creep vs plan original | `[feature-name o sprint-N]` |
|
|
226
227
|
| `/secret-management` | Gestion de secrets: GCP SM, Vault, SOPS | `[setup\|rotate\|audit]` |
|
|
227
|
-
| `/security-audit` | Audit de seguridad para juegos | `[full \| network \| save \| input \| quick]` |
|
|
228
228
|
| `/skill-improve` | Mejora un skill con loop test-fix-retest | `[skill-name]` |
|
|
229
229
|
| `/skill-test` | Valida skills (estructura + comportamiento) | `static [name\|all] \| spec [name] \| audit` |
|
|
230
230
|
| `/slack` | Gestiona Slack: mensajes, canales, archivos | `[send\|channel\|file\|search] <args>` |
|
package/docs/USER-GUIDE.md
CHANGED
|
@@ -93,7 +93,7 @@ If `.claude/` already exists, it gets backed up to `.claude.bak/` and replaced.
|
|
|
93
93
|
npx claude-code-arcane add api-design
|
|
94
94
|
|
|
95
95
|
# Add multiple skills
|
|
96
|
-
npx claude-code-arcane add
|
|
96
|
+
npx claude-code-arcane add owasp-top10-check secret-management
|
|
97
97
|
|
|
98
98
|
# Add an entire profile
|
|
99
99
|
npx claude-code-arcane add +security
|
|
@@ -141,6 +141,23 @@ npx claude-code-arcane status
|
|
|
141
141
|
|
|
142
142
|
Shows installed profiles, skill count, rules, agents, installation date, and worktree info.
|
|
143
143
|
|
|
144
|
+
### `npx claude-code-arcane doctor` — Check that the install works
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
npx claude-code-arcane doctor
|
|
148
|
+
npx claude-code-arcane doctor --target ../other-project
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Read-only. Each check prints ✓, `!` (warning) or ✗ (failure) with a fix:
|
|
152
|
+
|
|
153
|
+
- **Hooks:** every script `settings.json` runs exists and parses with bash; hooks still wired with a relative path, which fails inside a worktree.
|
|
154
|
+
- **Settings:** `settings.json` is valid JSON.
|
|
155
|
+
- **Content:** every skill and rule in the manifest is on disk.
|
|
156
|
+
- **Python:** one starts, since the guard and the session watch need it.
|
|
157
|
+
- **Permissions and secrets:** allow rules that skip every confirmation (`Bash(*)`, `Bash(gh *)`, `bypassPermissions`), and credentials in `settings.json`, `settings.local.json` or `.mcp.json`.
|
|
158
|
+
|
|
159
|
+
It exits with code 1 when a check fails.
|
|
160
|
+
|
|
144
161
|
### `npx claude-code-arcane update` — Check for updates
|
|
145
162
|
|
|
146
163
|
```bash
|
package/package.json
CHANGED
package/profiles/backend-go.yaml
CHANGED
package/profiles/security.yaml
CHANGED
package/profiles/unity-dev.yaml
CHANGED
package/profiles/unreal-dev.yaml
CHANGED
|
@@ -17,7 +17,7 @@ Add individual skills or entire profiles to the current Arcane installation with
|
|
|
17
17
|
Examples:
|
|
18
18
|
- `/arcane-add` — no args: show what can be added and ask
|
|
19
19
|
- `/arcane-add docker-setup` — add a single skill
|
|
20
|
-
- `/arcane-add
|
|
20
|
+
- `/arcane-add owasp-top10-check secret-management` — add multiple skills
|
|
21
21
|
- `/arcane-add +security` — add all skills from the +security profile
|
|
22
22
|
- `/arcane-add +database` — add all skills from the +database profile
|
|
23
23
|
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: security-audit
|
|
3
|
-
description: "
|
|
2
|
+
name: game-security-audit
|
|
3
|
+
description: "Game security audit (Unity/Unreal): save tampering, cheat vectors, network exploits, data exposure, input validation. Prioritized report + fixes."
|
|
4
4
|
category: "security"
|
|
5
5
|
argument-hint: "[full | network | save | input | quick]"
|
|
6
6
|
user-invocable: true
|
|
7
|
-
allowed-tools: Read, Glob, Grep, Bash, Write
|
|
7
|
+
allowed-tools: Read, Glob, Grep, Bash, Write
|
|
8
8
|
context: fork
|
|
9
9
|
agent: security-architect
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
# Security Audit
|
|
12
|
+
# Game Security Audit
|
|
13
13
|
|
|
14
|
-
Systematically audits codebase for common game security failures and produces prioritised remediation plan.
|
|
14
|
+
Systematically audits a game codebase for common game security failures and produces prioritised remediation plan.
|
|
15
15
|
|
|
16
16
|
**Run:** before public release (required for Polish→Release gate), before multiplayer features, after disk/network read systems, when security bug reported.
|
|
17
17
|
|
|
@@ -30,11 +30,7 @@ Output: `production/security/security-audit-[date].md`
|
|
|
30
30
|
|
|
31
31
|
Detect language, platforms and, for a game, the engine (`ProjectSettings/ProjectVersion.txt` (Unity) or the `.uproject` (Unreal)) from the project files. Take multiplayer scope from the GDD, or ask.
|
|
32
32
|
|
|
33
|
-
## Phase 2:
|
|
34
|
-
|
|
35
|
-
Spawn `security-engineer` via Task with: audit mode, engine/language, manifest of source directories. Collect full findings.
|
|
36
|
-
|
|
37
|
-
## Phase 3: Audit Categories
|
|
33
|
+
## Phase 2: Audit Categories
|
|
38
34
|
|
|
39
35
|
| Category | Key Checks |
|
|
40
36
|
|----------|-----------|
|
|
@@ -47,7 +43,7 @@ Spawn `security-engineer` via Task with: audit mode, engine/language, manifest o
|
|
|
47
43
|
|
|
48
44
|
Skip categories not applicable. Grep for relevant patterns per category.
|
|
49
45
|
|
|
50
|
-
## Phase
|
|
46
|
+
## Phase 3: Classify Findings
|
|
51
47
|
|
|
52
48
|
| Severity | Definition |
|
|
53
49
|
|----------|-----------|
|
|
@@ -58,17 +54,17 @@ Skip categories not applicable. Grep for relevant patterns per category.
|
|
|
58
54
|
|
|
59
55
|
Status: Open / Accepted Risk / Out of Scope.
|
|
60
56
|
|
|
61
|
-
## Phase
|
|
57
|
+
## Phase 4: Generate Report
|
|
62
58
|
|
|
63
|
-
Executive summary (severity counts + release recommendation: CLEAR TO SHIP / FIX CRITICALS / DO NOT SHIP). Per finding: ID, category, file+line, description, attack scenario, remediation, effort. Dependency inventory (plugin/version/source/CVEs). Prioritised remediation order.
|
|
59
|
+
Executive summary (severity counts + release recommendation: CLEAR TO SHIP / FIX CRITICALS / DO NOT SHIP, which read as PASS / CONCERNS / FAIL next to other audits). Per finding: ID, category, file+line, description, attack scenario, remediation, effort. Dependency inventory (plugin/version/source/CVEs). Prioritised remediation order.
|
|
64
60
|
|
|
65
|
-
## Phase
|
|
61
|
+
## Phase 5: Write Report
|
|
66
62
|
|
|
67
63
|
Present summary + CRITICAL/HIGH in conversation. "May I write full report to `production/security/security-audit-[date].md`?" Write after approval.
|
|
68
64
|
|
|
69
|
-
## Phase
|
|
65
|
+
## Phase 6: Gate Integration
|
|
70
66
|
|
|
71
|
-
Required for **Polish → Release gate**. After remediation: `/security-audit quick` to confirm. CRITICAL exists → "Must resolve before release." No CRITICAL/HIGH → "Clear for `/gate-check release`."
|
|
67
|
+
Required for **Polish → Release gate**. After remediation: `/game-security-audit quick` to confirm. CRITICAL exists → "Must resolve before release." No CRITICAL/HIGH → "Clear for `/gate-check release`."
|
|
72
68
|
|
|
73
69
|
## Protocol
|
|
74
70
|
|
|
@@ -446,6 +446,6 @@ Validate before considering the work done:
|
|
|
446
446
|
## Next Steps
|
|
447
447
|
|
|
448
448
|
- `/ue-testing-debugging` — test with multiple clients and simulated latency
|
|
449
|
-
- `/security-audit` — review the client-to-server trust boundary
|
|
449
|
+
- `/game-security-audit` — review the client-to-server trust boundary
|
|
450
450
|
- `/code-review` — architectural and quality review of the change
|
|
451
451
|
- `/commit` — conventional commit once the change is verified
|