{"record":{"id":"de4b404990dc9c83","repo":"different-ai/openwork","slug":"invalid-skill-name","errorCode":"invalid_skill_name","errorMessage":"Skill name must be kebab-case (1-64 chars)","messagePattern":"Skill name must be kebab-case \\(1-64 chars\\)","errorType":"validation","errorClass":"ApiError","httpStatus":400,"severity":"error","filePath":"apps/server/src/validators.ts","lineNumber":11,"sourceCode":"import { ApiError } from \"./errors.js\";\n\nconst SKILL_NAME_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;\nconst COMMAND_NAME_REGEX = /^[A-Za-z0-9_-]+$/;\nconst MCP_NAME_REGEX = /^[A-Za-z0-9_-]+$/;\nconst RESERVED_USER_MCP_NAMES = new Set([\"openwork-cloud\"]);\nconst RESERVED_USER_MCP_PREFIXES = [\"openwork-connect-\"];\n\nexport function validateSkillName(name: string): void {\n  if (!name || name.length < 1 || name.length > 64 || !SKILL_NAME_REGEX.test(name)) {\n    throw new ApiError(400, \"invalid_skill_name\", \"Skill name must be kebab-case (1-64 chars)\");\n  }\n}\n\nexport function validateDescription(description: string | undefined): void {\n  if (!description || description.length < 1 || description.length > 1024) {\n    throw new ApiError(422, \"invalid_description\", \"Description must be 1-1024 characters\");\n  }\n}\n\nexport function validatePluginSpec(spec: string): void {\n  if (!spec || spec.trim().length === 0) {\n    throw new ApiError(400, \"invalid_plugin_spec\", \"Plugin spec is required\");\n  }\n}\n\nexport function sanitizeCommandName(name: string): string {\n  const trimmed = name.trim().replace(/^\\/+/, \"\");\n  return trimmed;","sourceCodeStart":1,"sourceCodeEnd":29,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/apps/server/src/validators.ts#L1-L29","documentation":"This 400 invalid_skill_name error is thrown by validateSkillName when a skill name is empty, longer than 64 characters, or fails SKILL_NAME_REGEX (kebab-case pattern). Called from parseSkillEntry, buildSkillContent, and deleteSkill, it guards skill directory naming so names are filesystem- and URL-safe.","triggerScenarios":"Creating/updating a skill with a name containing spaces, uppercase-plus-invalid chars per the regex, or >64 chars; e.g. \"My Skill!\", \"a\".repeat(65), \"\"; also deleting with a malformed name.","commonSituations":"Auto-generated titles used verbatim as skill names; user input not sanitized; names with spaces copied from documentation; marketplace entries with ids that exceed 64 chars.","solutions":["Convert the name to kebab-case: lowercase, letters/digits/hyphens only, 1-64 chars","Trim and slugify user-provided titles before calling the API","Validate the name with the same regex client-side before the request"],"exampleFix":"// before\nconst name = title; // \"My Cool Skill!\"\n// after\nconst name = title.toLowerCase().replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\").slice(0, 64);","handlingStrategy":"validation","validationCode":"const SLUG = /^[a-z0-9]+(-[a-z0-9]+)*$/;\nif (!name || name.length > 64 || !SLUG.test(name)) throw new Error(\"use kebab-case, 1-64 chars\");","typeGuard":"const isValidSkillName = (name: string): boolean => name.length >= 1 && name.length <= 64 && /^[A-Za-z0-9_-]+$/.test(name);","tryCatchPattern":"try {\n  await createSkill({ name, ...rest });\n} catch (e) {\n  if (e.code === \"invalid_skill_name\") {\n    await createSkill({ name: slugify(name), ...rest });\n  } else throw e;\n}","preventionTips":["Slugify titles: lowercase, replace non-alphanumerics with hyphens, cap at 64 chars","Validate names with the same regex client-side before any skill API call","Never pass raw user input or generated titles as skill names"],"tags":["validation","naming","http-400"],"backgroundTag":"invalid-naming-format","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}