{"record":{"id":"b6d0eff3cbcfaf5c","repo":"github/spec-kit","slug":"integration-destination-dest-resolved-escapes-pr","errorCode":null,"errorMessage":"Integration destination {dest_resolved} escapes project root {project_root_resolved}","messagePattern":"Integration destination (.+?) escapes project root (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/integrations/copilot/__init__.py","lineNumber":511,"sourceCode":"            raise ValueError(\n                f\"manifest.project_root ({manifest.project_root}) does not match \"\n                f\"project_root ({project_root_resolved})\"\n            )\n\n        templates = self.list_command_templates()\n        if not templates:\n            return []\n\n        from ...presets import PresetResolver\n\n        preset_resolver = PresetResolver(project_root_resolved)\n\n        dest = self.commands_dest(project_root)\n        dest_resolved = dest.resolve()\n        try:\n            dest_resolved.relative_to(project_root_resolved)\n        except ValueError as exc:\n            raise ValueError(\n                f\"Integration destination {dest_resolved} escapes \"\n                f\"project root {project_root_resolved}\"\n            ) from exc\n        dest.mkdir(parents=True, exist_ok=True)\n        created: list[Path] = []\n\n        script_type = opts.get(\"script_type\", \"sh\")\n        arg_placeholder = self.registrar_config.get(\"args\", \"$ARGUMENTS\")\n\n        # 1. Process and write command files as .agent.md\n        for src_file in templates:\n            resolved_template = preset_resolver.resolve(\n                f\"speckit.{src_file.stem}\", template_type=\"command\"\n            )\n            source_path = resolved_template or src_file\n            raw = source_path.read_text(encoding=\"utf-8\")\n            processed = self.process_template(\n                raw, self.key, script_type, arg_placeholder,","sourceCodeStart":493,"sourceCodeEnd":529,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/integrations/copilot/__init__.py#L493-L529","documentation":"Raised by CopilotIntegration._setup_commands() (src/specify_cli/integrations/copilot/__init__.py:511) when the resolved commands destination (self.commands_dest(project_root).resolve()) does not lie under the resolved project root — Path.relative_to(project_root_resolved) raised ValueError, which is chained into this guard. It is a containment check preventing the integration from writing command files outside the project.","triggerScenarios":"Calling CopilotIntegration.setup(..., --commands mode) where commands_dest() resolves outside project_root — in practice caused by subclassing/overriding commands_dest(), or a symlinked destination directory pointing elsewhere, since the raw config folder (.github/agents) is normally in-tree.","commonSituations":"Custom integration subclass overriding commands_dest to an absolute or external path; a project directory containing a symlink like .github -> /somewhere/else; passing an inconsistent project_root that differs from the one used to build the destination.","solutions":["Ensure the destination returned by commands_dest(project_root) resolves under project_root.resolve().","Remove or repoint any symlinked destination directories (.github, .github/agents) inside the project.","Pass the same resolved project_root used elsewhere in the call.","If you subclassed CopilotIntegration, keep the destination relative and in-tree."],"exampleFix":"# before\nclass MyCopilot(CopilotIntegration):\n    def commands_dest(self, project_root):\n        return Path(\"/opt/shared/agents\")  # escapes project root\n\n# after\nclass MyCopilot(CopilotIntegration):\n    def commands_dest(self, project_root):\n        return project_root / \".github\" / \"agents\"","handlingStrategy":"validation","validationCode":"root = project_root.resolve()\ndest = integration.commands_dest(project_root).resolve()\ntry:\n    dest.relative_to(root)\nexcept ValueError:\n    raise SystemExit(f\"destination {dest} escapes {root}\")","typeGuard":null,"tryCatchPattern":"try:\n    created = integration.setup(project_root, manifest)\nexcept ValueError as e:\n    if \"escapes project root\" in str(e):\n        fix_symlinked_dest(project_root / \".github\")\n    else:\n        raise","preventionTips":["Keep commands_dest() implementations relative to project_root.","Check for and remove symlinked destination directories (.github, .github/agents).","Pass one consistently resolved project_root to all setup calls."],"tags":["filesystem","path-containment","security","integration-setup","spec-kit"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}