{"record":{"id":"66400a8f31a581e1","repo":"garrytan/gstack","slug":"commitskill-a-skill-named-opts-name-already","errorCode":null,"errorMessage":"commitSkill: a skill named \"${opts.name}\" already exists at ${dest}. Pick a different name or remove the existing skill first ($B skill rm ${opts.name}${opts.tier === 'global' ? ' --global' : ''}).","messagePattern":"commitSkill: a skill named \"(.+?)\" already exists at (.+?)\\. Pick a different name or remove the existing skill first \\(\\$B skill rm (.+?)(.+?)\\)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"browse/src/browser-skill-write.ts","lineNumber":163,"sourceCode":"  fs.mkdirSync(tierRoot, { recursive: true, mode: 0o755 });\n  const realTierRoot = fs.realpathSync(tierRoot);\n\n  const dest = path.join(realTierRoot, opts.name);\n  if (!isPathWithin(dest, realTierRoot)) {\n    // Should be impossible after validateSkillName, but defense in depth.\n    throw new Error(`commitSkill: destination \"${dest}\" escapes tier root.`);\n  }\n\n  // Refuse to clobber. Both regular dirs and symlinks count.\n  let destExists = false;\n  try {\n    fs.lstatSync(dest);\n    destExists = true;\n  } catch (err: any) {\n    if (err.code !== 'ENOENT') throw err;\n  }\n  if (destExists) {\n    throw new Error(\n      `commitSkill: a skill named \"${opts.name}\" already exists at ${dest}. ` +\n      `Pick a different name or remove the existing skill first ` +\n      `($B skill rm ${opts.name}${opts.tier === 'global' ? ' --global' : ''}).`,\n    );\n  }\n\n  fs.renameSync(opts.stagedDir, dest);\n  return dest;\n}\n\n// ─── Discard (cleanup on failure or reject) ─────────────────────\n\n/**\n * Remove the staged skill directory and its per-spawn wrapper. Called on\n * test failure (step 8 of /skillify) or approval rejection (step 9).\n *\n * Idempotent: missing dirs are not an error. Best-effort: failures are\n * swallowed (cleanup is fire-and-forget, not load-bearing).","sourceCodeStart":145,"sourceCodeEnd":181,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/browse/src/browser-skill-write.ts#L145-L181","documentation":"Thrown by commitSkill when fs.lstatSync(dest) succeeded (i.e. the destination already exists as a regular dir, file, or symlink). commitSkill refuses to clobber because the agent approval gate is supposed to surface name collisions before this point. The error message includes the exact conflicting path and the corresponding `$B skill rm` command (with --global if the target tier is global) so the user can resolve it directly.","triggerScenarios":"commitSkill for a name that already exists in the target tier; a prior /skillify installed the same name; a tombstone was restored to the same tier; a hand-authored skill already occupies that dir; a symlink at the dest path (lstatSync does not follow, so a symlink counts as 'exists').","commonSituations":"Re-running /skillify with the same name without first removing the prior skill; trying to install a project-tier skill that shadows a global-tier one being committed to global; collision after a tombstone restore; tierRoot contains a leftover directory from a partial previous commit.","solutions":["Remove the existing skill first: `$B skill rm <name>` (project) or `$B skill rm <name> --global`.","Pick a different name for the new skill.","If the existing entry is a stale symlink, remove it directly: `rm <dest>` (it is not a real skill so rm may not find it).","Confirm the prior skill was tombstoned: `$B skill list` should no longer show it."],"exampleFix":"// before: ~/.gstack/browser-skills/foo already exists\ncommitSkill({ name: 'foo', tier: 'global', stagedDir });\n// after\n// step 1: $B skill rm foo --global\n// step 2: commitSkill({ name: 'foo', tier: 'global', stagedDir });","handlingStrategy":"validation","validationCode":"import * as fs from 'fs';\nimport * as path from 'path';\nimport { defaultTierPaths } from './browser-skills';\n\nfunction assertDestFree(name: string, tier: 'project' | 'global', tiers = defaultTierPaths()): void {\n  const root = tier === 'project' ? tiers.project : tiers.global;\n  if (!root) throw new Error(`Tier \"${tier}\" unresolved.`);\n  const dest = path.join(root, name);\n  try {\n    fs.lstatSync(dest);\n    throw new Error(`\"${name}\" already exists at ${dest}. Run: $B skill rm ${name}${tier === 'global' ? ' --global' : ''}`);\n  } catch (err: any) {\n    if (err.code === 'ENOENT') return; // free\n    throw err;\n  }\n}\n// before commitSkill:\nassertDestFree(opts.name, opts.tier);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Surface name collisions at the approval gate, before commitSkill runs.","Run `$B skill rm <name> [--global]` before re-installing over an existing skill.","Use a unique name (e.g. suffix with a version) when iterating on agent-authored skills."],"tags":["filesystem","skill-management","gstack","configuration"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}