{"record":{"id":"d6081183501416b6","repo":"affaan-m/ECC","slug":"refusing-to-action-outside-the-install-root","errorCode":null,"errorMessage":"Refusing to ${action} outside the install root: '${targetPath}' is not within '${targetRoot}'.","messagePattern":"Refusing to (.+?) outside the install root: '(.+?)' is not within '(.+?)'\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"scripts/lib/install/claude-skill-migration.js","lineNumber":56,"sourceCode":"function comparablePath(filePath) {\n  const resolvedPath = path.resolve(filePath);\n  return process.platform === 'win32' ? resolvedPath.toLowerCase() : resolvedPath;\n}\n\nfunction samePath(leftPath, rightPath) {\n  return comparablePath(leftPath) === comparablePath(rightPath);\n}\n\nfunction assertSafeSkillPath(targetPath, targetRoot, action) {\n  const resolvedRoot = path.resolve(targetRoot);\n  const resolvedTarget = path.resolve(targetPath);\n  const relativePath = path.relative(resolvedRoot, resolvedTarget);\n  if (\n    relativePath === ''\n    || relativePath.startsWith('..')\n    || path.isAbsolute(relativePath)\n  ) {\n    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(","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/scripts/lib/install/claude-skill-migration.js#L38-L74","documentation":"Thrown by assertSafeSkillPath in scripts/lib/install/claude-skill-migration.js — the Claude-skill-specific containment guard used for install, inspect, migrate, and cleanup actions. After computing relativePath = path.relative(targetRoot, targetPath), the guard throws if relativePath is '' (paths identical), starts with '..', or is absolute (which on Windows can indicate a different drive). This refuses writes that would escape the install root.","triggerScenarios":"A Claude skill operation whose destinationPath resolves above the install root, onto a different drive (Windows), or is identical to the targetRoot itself. Common with a maliciously crafted or corrupted install-state file (see the GHSA note in path-safety.js).","commonSituations":"A tampered install-state.json that recorded an absolute path outside .claude; Windows drive-letter mismatch; an operation destination built from raw user input without path.join normalization; replaying an old state under a different targetRoot.","solutions":["Print operation.destinationPath and plan.targetRoot side by side; confirm both are absolute and normalized.","Ensure destinations are built via path.join(targetRoot, ...) rather than from raw external input.","On Windows, confirm both paths are on the same drive and use the same casing.","Discard the suspect install-state and regenerate the plan from the adapter."],"exampleFix":"// before\noperations.push({\n  kind: 'copy-file',\n  sourcePath,\n  destinationPath: '/etc/passwd',\n});\n\n// after\noperations.push({\n  kind: 'copy-file',\n  sourcePath,\n  destinationPath: path.join(targetRoot, 'skills', 'foo', 'SKILL.md'),\n});","handlingStrategy":"validation","validationCode":"function assertWithin(parent, child) {\n  const rel = path.relative(path.resolve(parent), path.resolve(child));\n  if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) {\n    throw new Error(`Refusing: ${child} is outside ${parent}`);\n  }\n}\nfor (const op of plan.operations) assertWithin(plan.targetRoot, op.destinationPath);","typeGuard":null,"tryCatchPattern":"try {\n  applyInstallPlan(plan);\n} catch (err) {\n  if (/outside the install root/.test(err.message)) {\n    console.error('Path escape detected — operation list:', plan.operations);\n  }\n  throw err;\n}","preventionTips":["Always derive destination paths via path.join(targetRoot, ...) — never from raw external input.","On Windows, normalize drive-letter casing before comparing paths.","Never trust paths read from install-state files; re-derive them from the adapter root."],"tags":["security","claude-skill","path-safety","install-safety"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}