{"record":{"id":"7e7fad6a9b607d3a","repo":"garrytan/gstack","slug":"invalid-skill-name-name-must-be-lowercase-le","errorCode":null,"errorMessage":"Invalid skill name \"${name}\". Must be lowercase letters/digits/dashes, start with a letter, no leading/trailing/consecutive dashes.","messagePattern":"Invalid skill name \"(.+?)\"\\. Must be lowercase letters/digits/dashes, start with a letter, no leading/trailing/consecutive dashes\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"browse/src/browser-skill-write.ts","lineNumber":40,"sourceCode":"import { mkdirSecure } from './file-permissions';\nimport { isPathWithin } from './platform';\nimport type { TierPaths } from './browser-skills';\nimport { defaultTierPaths } from './browser-skills';\n\n// ─── Naming validation ──────────────────────────────────────────\n\n/**\n * Skill names must be safe directory names: lowercase letters, digits, dashes.\n * Starts with a letter, no consecutive dashes, no trailing dash, ≤64 chars.\n * Rejects '..', leading dots, slashes, anything that could escape the tier dir.\n */\nconst SKILL_NAME_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;\n\nexport function validateSkillName(name: string): void {\n  if (!name) throw new Error('Skill name is empty.');\n  if (name.length > 64) throw new Error(`Skill name too long (${name.length} > 64).`);\n  if (!SKILL_NAME_PATTERN.test(name)) {\n    throw new Error(\n      `Invalid skill name \"${name}\". Must be lowercase letters/digits/dashes, ` +\n      `start with a letter, no leading/trailing/consecutive dashes.`,\n    );\n  }\n}\n\n// ─── Staging ────────────────────────────────────────────────────\n\nexport interface StageSkillOptions {\n  name: string;\n  /** Map of relative path → contents. Path may contain '/' for nested dirs. */\n  files: Map<string, string | Buffer>;\n  /** Optional override (tests pass synthetic spawn ids). */\n  spawnId?: string;\n  /** Optional override (tests pass a fake tmp root). */\n  tmpRoot?: string;\n}\n","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/browse/src/browser-skill-write.ts#L22-L58","documentation":"Thrown by validateSkillName when the name is non-empty, ≤64 chars, but does not match /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/. The pattern enforces: lowercase only, must start with a letter, digits allowed after the first letter, single dashes between alphanumeric groups, no leading/trailing/consecutive dashes. This bound is load-bearing — it is what makes the leaf directory name safe to join onto a tier root without escaping.","triggerScenarios":"validateSkillName with values like 'HN_Frontpage' (uppercase + underscore), '1st-skill' (leading digit), '-foo' (leading dash), 'foo--bar' (consecutive dashes), 'foo-' (trailing dash), 'foo.bar' (dot), 'foo/bar' (slash), 'foo bar' (space), or a name with non-ASCII characters.","commonSituations":"Agent-derived name from a page title preserved casing or spaces; user typed a CamelCase name; templating produced a leading dash; name was sanitized with a different ruleset (underscores instead of dashes); Unicode host-derived name.","solutions":["Normalize the name to lowercase, replace runs of non-alphanum with a single dash, strip leading/trailing dashes.","Pick a name like 'hn-frontpage' that matches the pattern exactly.","If the name comes from external input, run a slugify step before validateSkillName."],"exampleFix":"// before\nvalidateSkillName('HN Frontpage!');\n// after\nfunction slugify(s: string): string {\n  return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');\n}\nvalidateSkillName(slugify('HN Frontpage!')); // 'hn-frontpage'","handlingStrategy":"validation","validationCode":"const SKILL_NAME_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;\n\nfunction slugifySkillName(raw: string): string {\n  const slug = raw.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');\n  if (!slug || !SKILL_NAME_PATTERN.test(slug)) {\n    throw new Error(`Cannot derive valid skill name from \"${raw}\"`);\n  }\n  return slug;\n}\n// use before validateSkillName:\nconst name = slugifySkillName(rawInput);","typeGuard":"const isValidSkillName = (x: unknown): x is string =>\n  typeof x === 'string' && /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(x) && x.length <= 64;","tryCatchPattern":null,"preventionTips":["Always slugify external input (page titles, agent output) before passing to validateSkillName.","Lowercase first, then replace non-alphanum runs with single dashes, then trim dashes.","Test the slugify step against realistic inputs in your wrapper."],"tags":["validation","security","skill-management","gstack"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}