Skip to content

Commit 948c099

Browse files
fix: validate version field is valid semver
Non-semver version strings (e.g., "2024.03.31.01") pass validation today but cause fatal unrecoverable crashes in Claude Desktop at bootstrap. Add semver validation to catch these before pack/distribution. Fixes #226 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e540835 commit 948c099

4 files changed

Lines changed: 112 additions & 0 deletions

File tree

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"@types/jest": "^29.5.14",
7171
"@types/node": "^22.15.3",
7272
"@types/node-forge": "1.3.11",
73+
"@types/semver": "^7.7.1",
7374
"@typescript-eslint/eslint-plugin": "^5.62.0",
7475
"@typescript-eslint/parser": "^5.62.0",
7576
"eslint": "^8.43.0",
@@ -91,6 +92,7 @@
9192
"ignore": "^7.0.5",
9293
"node-forge": "^1.3.2",
9394
"pretty-bytes": "^5.6.0",
95+
"semver": "^7.7.4",
9496
"zod": "^3.25.67",
9597
"zod-to-json-schema": "^3.24.6"
9698
},

src/node/validate.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { DestroyerOfModules } from "galactus";
44
import * as os from "os";
55
import { dirname, extname, isAbsolute, join, resolve } from "path";
66
import prettyBytes from "pretty-bytes";
7+
import semver from "semver";
78

89
import { unpackExtension } from "../cli/unpack.js";
910
import {
@@ -238,6 +239,25 @@ function validateCommandVariables(manifest: {
238239
return { valid: errors.length === 0, errors, warnings };
239240
}
240241

242+
/**
243+
* Validate that the version field is a valid semantic version.
244+
* Non-semver versions cause fatal crashes in Claude Desktop.
245+
*/
246+
function validateVersion(version: string): ValidationResult {
247+
const errors: string[] = [];
248+
const warnings: string[] = [];
249+
250+
if (!semver.valid(version)) {
251+
errors.push(
252+
`Version "${version}" is not valid semver. ` +
253+
`Expected format: MAJOR.MINOR.PATCH (e.g., "1.0.0", "2.1.0-beta.1"). ` +
254+
`Non-semver versions cause fatal crashes in Claude Desktop.`,
255+
);
256+
}
257+
258+
return { valid: errors.length === 0, errors, warnings };
259+
}
260+
241261
// Sensitive file patterns not already covered by EXCLUDE_PATTERNS in files.ts
242262
const SENSITIVE_PATTERNS = [
243263
/(^|\/)credentials\.json$/i,
@@ -323,6 +343,16 @@ export function validateManifest(
323343
: manifestDir;
324344
let hasErrors = false;
325345

346+
// Validate version format (non-semver crashes Claude Desktop)
347+
const versionValidation = validateVersion(manifestData.version);
348+
if (versionValidation.errors.length > 0) {
349+
console.log("\nERROR: Version validation failed:\n");
350+
versionValidation.errors.forEach((error) => {
351+
console.log(` - ${error}`);
352+
});
353+
hasErrors = true;
354+
}
355+
326356
// Validate icon if present (always relative to manifest directory)
327357
if (manifestData.icon) {
328358
const iconValidation = validateIcon(manifestData.icon, manifestDir);

test/validate.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,68 @@ describe("Enhanced Validation", () => {
350350
});
351351
});
352352

353+
describe("version validation", () => {
354+
it("should reject non-semver calver version", () => {
355+
const dir = join(fixturesDir, "version-calver");
356+
fs.mkdirSync(join(dir, "server"), { recursive: true });
357+
fs.writeFileSync(join(dir, "server", "index.js"), "// fixture");
358+
createManifest(dir, { version: "2024.03.31.01" });
359+
360+
try {
361+
execSync(`node ${cliPath} validate ${dir}`, { encoding: "utf-8" });
362+
fail("Expected validation to fail");
363+
} catch (e) {
364+
const error = e as ExecSyncError;
365+
expect(error.status).toBe(1);
366+
expect(error.stdout.toString()).toContain("not valid semver");
367+
}
368+
});
369+
370+
it("should reject non-numeric version string", () => {
371+
const dir = join(fixturesDir, "version-string");
372+
fs.mkdirSync(join(dir, "server"), { recursive: true });
373+
fs.writeFileSync(join(dir, "server", "index.js"), "// fixture");
374+
createManifest(dir, { version: "latest" });
375+
376+
try {
377+
execSync(`node ${cliPath} validate ${dir}`, { encoding: "utf-8" });
378+
fail("Expected validation to fail");
379+
} catch (e) {
380+
const error = e as ExecSyncError;
381+
expect(error.status).toBe(1);
382+
expect(error.stdout.toString()).toContain("not valid semver");
383+
}
384+
});
385+
386+
it("should accept standard semver version", () => {
387+
const dir = join(fixturesDir, "version-semver");
388+
fs.mkdirSync(join(dir, "server"), { recursive: true });
389+
fs.writeFileSync(join(dir, "server", "index.js"), "// fixture");
390+
createManifest(dir, { version: "1.0.0" });
391+
392+
const result = execSync(`node ${cliPath} validate ${dir}`, {
393+
encoding: "utf-8",
394+
});
395+
396+
expect(result).toContain("Manifest schema validation passes!");
397+
expect(result).not.toContain("not valid semver");
398+
});
399+
400+
it("should accept semver with prerelease and build metadata", () => {
401+
const dir = join(fixturesDir, "version-prerelease");
402+
fs.mkdirSync(join(dir, "server"), { recursive: true });
403+
fs.writeFileSync(join(dir, "server", "index.js"), "// fixture");
404+
createManifest(dir, { version: "2.1.0-beta.1+build.123" });
405+
406+
const result = execSync(`node ${cliPath} validate ${dir}`, {
407+
encoding: "utf-8",
408+
});
409+
410+
expect(result).toContain("Manifest schema validation passes!");
411+
expect(result).not.toContain("not valid semver");
412+
});
413+
});
414+
353415
describe("happy path", () => {
354416
it("should pass with all files present and correct types", () => {
355417
const dir = join(fixturesDir, "happy-path");

yarn.lock

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ __metadata:
2323
"@types/jest": "npm:^29.5.14"
2424
"@types/node": "npm:^22.15.3"
2525
"@types/node-forge": "npm:1.3.11"
26+
"@types/semver": "npm:^7.7.1"
2627
"@typescript-eslint/eslint-plugin": "npm:^5.62.0"
2728
"@typescript-eslint/parser": "npm:^5.62.0"
2829
commander: "npm:^13.1.0"
@@ -39,6 +40,7 @@ __metadata:
3940
node-forge: "npm:^1.3.2"
4041
prettier: "npm:^3.3.3"
4142
pretty-bytes: "npm:^5.6.0"
43+
semver: "npm:^7.7.4"
4244
ts-jest: "npm:^29.3.2"
4345
typescript: "npm:^5.6.3"
4446
zod: "npm:^3.25.67"
@@ -1257,6 +1259,13 @@ __metadata:
12571259
languageName: node
12581260
linkType: hard
12591261

1262+
"@types/semver@npm:^7.7.1":
1263+
version: 7.7.1
1264+
resolution: "@types/semver@npm:7.7.1"
1265+
checksum: 10c0/c938aef3bf79a73f0f3f6037c16e2e759ff40c54122ddf0b2583703393d8d3127130823facb880e694caa324eb6845628186aac1997ee8b31dc2d18fafe26268
1266+
languageName: node
1267+
linkType: hard
1268+
12601269
"@types/stack-utils@npm:^2.0.0":
12611270
version: 2.0.3
12621271
resolution: "@types/stack-utils@npm:2.0.3"
@@ -5394,6 +5403,15 @@ __metadata:
53945403
languageName: node
53955404
linkType: hard
53965405

5406+
"semver@npm:^7.7.4":
5407+
version: 7.7.4
5408+
resolution: "semver@npm:7.7.4"
5409+
bin:
5410+
semver: bin/semver.js
5411+
checksum: 10c0/5215ad0234e2845d4ea5bb9d836d42b03499546ddafb12075566899fc617f68794bb6f146076b6881d755de17d6c6cc73372555879ec7dce2c2feee947866ad2
5412+
languageName: node
5413+
linkType: hard
5414+
53975415
"set-function-length@npm:^1.2.2":
53985416
version: 1.2.2
53995417
resolution: "set-function-length@npm:1.2.2"

0 commit comments

Comments
 (0)