{"record":{"id":"eb9afd39b0ff8d7b","repo":"affaan-m/ECC","slug":"refusing-to-action-through-symlinked-claude-ski","errorCode":null,"errorMessage":"Refusing to ${action} through symlinked Claude skill path: '${currentPath}'.","messagePattern":"Refusing to (.+?) through symlinked Claude skill path: '(.+?)'\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"scripts/lib/install/claude-skill-migration.js","lineNumber":74,"sourceCode":"    throw new Error(\n      `Refusing to ${action} outside the install root: '${targetPath}' is not within '${targetRoot}'.`\n    );\n  }\n\n  let currentPath = resolvedRoot;\n  for (const segment of relativePath.split(path.sep)) {\n    currentPath = path.join(currentPath, segment);\n    let stats;\n    try {\n      stats = fs.lstatSync(currentPath);\n    } catch (error) {\n      if (error && error.code === 'ENOENT') {\n        break;\n      }\n      throw error;\n    }\n    if (stats.isSymbolicLink()) {\n      throw new Error(\n        `Refusing to ${action} through symlinked Claude skill path: '${currentPath}'.`\n      );\n    }\n  }\n\n  if (pathExists(targetRoot)) {\n    assertWithinTrustedRoot(targetPath, targetRoot, action);\n  }\n}\n\nfunction describeClaudeSkillOperation(targetRoot, operation) {\n  if (!operation || operation.kind !== 'copy-file') {\n    return null;\n  }\n\n  const sourceRelativePath = normalizeSourceRelativePath(operation.sourceRelativePath);\n  if (!sourceRelativePath) {\n    return null;","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/lib/install/claude-skill-migration.js#L56-L92","documentation":"assertSafeSkillPath walks each path segment from the install root toward the target and refuses to proceed if any intermediate component is a symbolic link (ENOENT segments are tolerated). This prevents skill operations from following symlinks to locations outside the managed root.","triggerScenarios":"Any skill migration/removal call where the target path — or a parent directory along the way — is a symlink, e.g. skills dir symlinked into a dotfiles repo, or a per-skill symlink pointing elsewhere.","commonSituations":"Users who symlink ~/.claude/skills into a version-controlled dotfiles directory, or symlink individual skills for sharing; the migration code intentionally refuses to operate through these links.","solutions":["Replace the symlink with a real directory (copy or move the content) before running the migration.","Temporarily materialize the path: remove the link, copy contents, rerun the operation, then re-create the symlink if needed afterward.","Point the install root at the real (non-symlinked) location where the files physically live.","If only one child is a symlink, unlink and copy that specific skill into place."],"exampleFix":"// before: ~/.claude/skills -> ~/dotfiles/claude-skills (symlink)\n// after\nrm ~/.claude/skills\ncp -r ~/dotfiles/claude-skills ~/.claude/skills","handlingStrategy":"validation","validationCode":"const fs = require('fs');\nfunction hasSymlinkInPath(root, target) {\n  let cur = path.resolve(root);\n  const dest = path.resolve(target);\n  const rel = path.relative(cur, dest);\n  for (const seg of rel.split(path.sep)) {\n    cur = path.join(cur, seg);\n    try { if (fs.lstatSync(cur).isSymbolicLink()) return true; }\n    catch (e) { if (e.code === 'ENOENT') break; throw e; }\n  }\n  return false;\n}","typeGuard":"const isSymlinkFree = (root, target) => !hasSymlinkInPath(root, target);","tryCatchPattern":"try {\n  await assertSafeClaudeSkillOperation({ action: 'migrate', targetPath, targetRoot });\n} catch (error) {\n  if (error.message.includes('symlinked Claude skill path')) {\n    console.error('Replace the symlink with a real directory, rerun, then re-link if desired');\n    return;\n  }\n  throw error;\n}","preventionTips":["Avoid symlinking ~/.claude/skills into dotfiles repos if you run the installer/migration","Check `ls -la` along the target path before running skill operations","Materialize symlinks into copies before migration, re-link afterward if needed","Point install root at the physical directory, not a symlinked path"],"tags":["symlink","security","filesystem","safety-guard"],"backgroundTag":"path-traversal-blocked","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}