{"record":{"id":"18935028178262f9","repo":"ReactiveX/rxjs","slug":"message-189350","errorCode":null,"errorMessage":"${message}","messagePattern":"\\$\\{message\\}","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/migrate/src/skill-install.ts","lineNumber":355,"sourceCode":"): SkillInstallResult {\n  return {\n    action,\n    adapter: installation.adapter,\n    compatibleHarnesses: installation.compatibleHarnesses,\n    targetPath: installation.targetPath,\n    provenancePath: installation.provenancePath,\n    stateBefore,\n    stateAfter,\n    changed,\n    provenance: installation.provenance,\n    canonicalIntegrity: installation.provenance,\n  };\n}\n\nfunction assertContained(root: string, candidate: string, message: string): void {\n  const localPath = relative(root, candidate);\n  if (localPath === '' || (localPath !== '..' && !localPath.startsWith(`..${sep}`) && !isAbsolute(localPath))) return;\n  throw new Error(message);\n}\n\nasync function canonicalFuturePath(path: string): Promise<string> {\n  const missing: string[] = [];\n  let existing = path;\n  let canonicalExisting: string | undefined;\n  while (canonicalExisting === undefined) {\n    try {\n      canonicalExisting = await realpath(existing);\n    } catch (error: unknown) {\n      if (!isMissingPathError(error)) throw error;\n      const parent = dirname(existing);\n      if (parent === existing) throw error;\n      missing.push(relative(parent, existing));\n      existing = parent;\n    }\n  }\n  return resolve(canonicalExisting, ...missing.reverse());","sourceCodeStart":337,"sourceCodeEnd":373,"githubUrl":"https://github.com/ReactiveX/rxjs/blob/54796b38a57e6309f9861e174737479bb3f63f61/packages/migrate/src/skill-install.ts#L337-L373","documentation":"Thrown by skill-install's assertContained when a skill installation target path resolves equal to the project root or outside it. It is the same path-traversal guard as the CLI version, applied to skill targets like .claude/skills/... to guarantee installs stay within the project.","triggerScenarios":"An adapter whose localPath is '' or contains '..' so targetPath equals or escapes projectRoot; a canonicalFuturePath result that symlinks outside canonicalProjectRoot; custom adapters with bad targetDirectory values.","commonSituations":"Custom/buggy harness adapters; symlinks in the target path chain leading outside the repo; unusual adapter configuration.","solutions":["Use the built-in harness adapters (claude/codex/cursor) rather than custom ones with relative escape paths","Remove symlinks under the skill target path that point outside the project","Set the adapter targetDirectory to a normal in-project relative path like '.claude/skills/rxjs-migrate'"],"exampleFix":"// before\nconst adapter = { adapterId: 'x', targetDirectory: '../outside', compatibleHarnesses: ['x'] };\n// after\nconst adapter = { adapterId: 'x', targetDirectory: '.x/skills/rxjs-migrate', compatibleHarnesses: ['x'] };","handlingStrategy":"validation","validationCode":"import { relative, isAbsolute } from 'node:path';\nconst r = relative(projectRoot, targetPath);\nif (r === '' || r === '..' || r.startsWith(`..${sep}`) || isAbsolute(r)) throw new TypeError('skill target escapes project root');","typeGuard":"const isContainedPath = (root: string, candidate: string): boolean => { const r = relative(root, candidate); return r !== '' && r !== '..' && !r.startsWith(`..${sep}`) && !isAbsolute(r); };","tryCatchPattern":null,"preventionTips":["Use built-in harness adapters","Keep skill targetDirectory as a plain in-project relative path","Avoid symlinks inside the skill target chain"],"tags":["path-traversal","skill-install","filesystem","validation"],"backgroundTag":"path-traversal-guard","analyzedSha":"54796b38a57e6309f9861e174737479bb3f63f61","analyzedAt":"2026-08-28T10:21:27.410Z","schemaVersion":2},"datasetVersion":"2026-08-28T11:17:15.048Z"}