{"record":{"id":"8430e459586cbbc0","repo":"garrytan/gstack","slug":"commitskill-staged-path-opts-stageddir-is-no","errorCode":null,"errorMessage":"commitSkill: staged path \"${opts.stagedDir}\" is not a directory.","messagePattern":"commitSkill: staged path \"(.+?)\" is not a directory\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"browse/src/browser-skill-write.ts","lineNumber":140,"sourceCode":"  const tiers = opts.tiers ?? defaultTierPaths();\n  const tierRoot = opts.tier === 'project' ? tiers.project : tiers.global;\n  if (!tierRoot) {\n    throw new Error(`commitSkill: tier \"${opts.tier}\" has no resolved path.`);\n  }\n\n  // Refuse to follow a symlinked staging dir — caller should hand us the path\n  // returned by stageSkill, which is always a real directory.\n  let stagedStat: fs.Stats;\n  try {\n    stagedStat = fs.lstatSync(opts.stagedDir);\n  } catch (err: any) {\n    throw new Error(`commitSkill: staged dir \"${opts.stagedDir}\" not accessible: ${err.code ?? err.message}`);\n  }\n  if (stagedStat.isSymbolicLink()) {\n    throw new Error(`commitSkill: staged dir \"${opts.stagedDir}\" is a symlink — refusing to commit.`);\n  }\n  if (!stagedStat.isDirectory()) {\n    throw new Error(`commitSkill: staged path \"${opts.stagedDir}\" is not a directory.`);\n  }\n\n  // Ensure the tier root exists, then resolve its real path so the final\n  // destination check defends against tierRoot itself being a symlink.\n  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;","sourceCodeStart":122,"sourceCodeEnd":158,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/browse/src/browser-skill-write.ts#L122-L158","documentation":"Thrown by commitSkill when lstatSync succeeded, the path is not a symlink, but stagedStat.isDirectory() is false — the staged path resolves to a regular file (or a socket/device), not a directory. fs.renameSync would still move it, but the result would not be a usable skill directory, so commitSkill bails first.","triggerScenarios":"A caller passed a path to a file (e.g. the SKILL.md path rather than the skill dir); stagedDir was a directory at stage time but a file got written over it; tests that constructed stagedDir pointing at a file.","commonSituations":"Caller joined one path segment too many (e.g. path.join(stagingRoot, name, 'SKILL.md') instead of path.join(stagingRoot, name)); another process overwrote the dir entry with a file; misconfigured /skillify driver that points at the wrong level of the staging tree.","solutions":["Pass the directory path returned by stageSkill (ending in the skill name), not a file inside it.","Verify with `ls -ld <stagedDir>` that it is a directory before calling commitSkill.","If a file overwrote the dir, remove it and re-run stageSkill."],"exampleFix":"// before\ncommitSkill({ name: 'foo', tier: 'project', stagedDir: '/path/foo/SKILL.md' });\n// after\ncommitSkill({ name: 'foo', tier: 'project', stagedDir: '/path/foo' });","handlingStrategy":"validation","validationCode":"import * as fs from 'fs';\n\nfunction assertStagedIsDirectory(stagedDir: string): void {\n  const stat = fs.lstatSync(stagedDir);\n  if (!stat.isDirectory()) {\n    throw new Error(`Staged path \"${stagedDir}\" is not a directory (got ${`+${stat.mode.toString(8)}`}).`);\n  }\n}\n// before commitSkill:\nassertStagedIsDirectory(opts.stagedDir);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Pass the directory path ending in the skill name, not a file inside it.","Verify with `ls -ld <stagedDir>` before commit.","Do not let another process overwrite the dir entry with a file mid-flow."],"tags":["filesystem","validation","skill-management","gstack"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}