{"record":{"id":"7a75f9d198e2b633","repo":"garrytan/gstack","slug":"commitskill-staged-dir-opts-stageddir-is-a-s","errorCode":null,"errorMessage":"commitSkill: staged dir \"${opts.stagedDir}\" is a symlink — refusing to commit.","messagePattern":"commitSkill: staged dir \"(.+?)\" is a symlink — refusing to commit\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"browse/src/browser-skill-write.ts","lineNumber":137,"sourceCode":"export function commitSkill(opts: CommitSkillOptions): string {\n  validateSkillName(opts.name);\n\n  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;","sourceCodeStart":119,"sourceCodeEnd":155,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/browse/src/browser-skill-write.ts#L119-L155","documentation":"Thrown by commitSkill when lstatSync succeeded but stagedStat.isSymbolicLink() returned true. stageSkill always returns a real directory created via mkdirSecure, so a symlinked staged dir means a caller bypassed stageSkill or something replaced the dir after staging. Refusing to commit a symlink prevents a rename-time escape where the symlink target sits outside the staging tree.","triggerScenarios":"A caller hand-constructed stagedDir as `ln -s /elsewhere foo` and passed it to commitSkill; a malicious or buggy agent replaced the staged dir with a symlink between stageSkill returning and commitSkill being called; tests that mock stagedDir with a symlink.","commonSituations":"Adversarial agent attempting to write outside the tier root by symlink substitution; misconfigured test fixture using symlinks for convenience; downstream tool that 'helpfully' symlinks hot-reload paths over the staged dir.","solutions":["Always pass the exact path returned by stageSkill — never reconstruct or replace it.","If a symlink is present, remove it and re-run stageSkill to recreate a real directory.","Audit any tooling that touches ~/.gstack/.tmp/skillify-<spawnId>/ between stage and commit."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import * as fs from 'fs';\n\nfunction assertStagedIsRealDir(stagedDir: string): void {\n  const stat = fs.lstatSync(stagedDir);\n  if (stat.isSymbolicLink()) {\n    throw new Error(`Staged dir \"${stagedDir}\" is a symlink — refusing to commit.`);\n  }\n}\n// before commitSkill:\nassertStagedIsRealDir(opts.stagedDir);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never replace the path returned by stageSkill with a symlink.","Audit any tooling that touches ~/.gstack/.tmp/skillify-<spawnId>/ between stage and commit.","Treat a symlinked staged dir as a security signal — investigate before re-staging."],"tags":["security","filesystem","skill-management","gstack"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}