{"record":{"id":"e4bab8ecc323edfb","repo":"github/spec-kit","slug":"integration-destination-dest-escapes-project-roo-e4bab8","errorCode":null,"errorMessage":"Integration destination {dest} 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/generic/__init__.py","lineNumber":124,"sourceCode":"        \"\"\"Install commands to the user-provided commands directory.\"\"\"\n        commands_dir = self._resolve_commands_dir(parsed_options, opts)\n\n        templates = self.list_command_templates()\n        if not templates:\n            return []\n\n        project_root_resolved = project_root.resolve()\n        if manifest.project_root != project_root_resolved:\n            raise ValueError(\n                f\"manifest.project_root ({manifest.project_root}) does not match \"\n                f\"project_root ({project_root_resolved})\"\n            )\n\n        dest = (project_root / commands_dir).resolve()\n        try:\n            dest.relative_to(project_root_resolved)\n        except ValueError as exc:\n            raise ValueError(\n                f\"Integration destination {dest} escapes \"\n                f\"project root {project_root_resolved}\"\n            ) from exc\n        dest.mkdir(parents=True, exist_ok=True)\n\n        script_type = opts.get(\"script_type\", \"sh\")\n        arg_placeholder = \"$ARGUMENTS\"\n        created: list[Path] = []\n\n        for src_file in templates:\n            raw = src_file.read_text(encoding=\"utf-8\")\n            processed = self.process_template(\n                raw, self.key, script_type, arg_placeholder,\n                project_root=project_root,\n            )\n            dst_name = self.command_filename(src_file.stem)\n            dst_file = self.write_file_and_record(\n                processed, dest / dst_name, project_root, manifest","sourceCodeStart":106,"sourceCodeEnd":142,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/integrations/generic/__init__.py#L106-L142","documentation":"Raised by GenericIntegration.setup() (src/specify_cli/integrations/generic/__init__.py:124) when (project_root / commands_dir).resolve() falls outside project_root_resolved — the relative_to containment check threw and was chained. Because commands_dir is user-supplied, this guard is the critical defense against '../' or absolute values writing outside the project.","triggerScenarios":"Passing parsed_options={\"commands_dir\": \"../outside\"} or an absolute path like \"/etc/agent\"; or a commands_dir whose first component is a symlink pointing outside the project, so resolve() escapes the root.","commonSituations":"Users trying to install commands into a shared/global directory; dotfile-symlinked directories (e.g. .myagent -> ~/dotfiles/.myagent); malformed --commands-dir values with leading slashes or '..'.","solutions":["Use a relative commands_dir without '..' that stays inside the project (e.g. '.myagent/commands').","Remove destination symlinks that resolve outside the project.","If a global install is desired, run specify per-project instead of pointing commands_dir outside."],"exampleFix":"# before\nspecify init --integration generic --integration-options=\"--commands-dir /usr/share/agent\"\n\n# after\nspecify init --integration generic --integration-options=\"--commands-dir .agent/commands\"","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\nroot = project_root.resolve()\ncommands_dir = opts[\"commands_dir\"]\ndest = (root / commands_dir).resolve()\ntry:\n    dest.relative_to(root)\nexcept ValueError:\n    raise SystemExit(f\"commands_dir {commands_dir!r} escapes project root\")","typeGuard":"def is_in_project(root: Path, rel: str) -> bool:\n    try:\n        (root / rel).resolve().relative_to(root.resolve())\n        return True\n    except ValueError:\n        return False","tryCatchPattern":"try:\n    integration.setup(root, manifest, parsed_options=opts)\nexcept ValueError as e:\n    if \"escapes project root\" in str(e):\n        opts[\"commands_dir\"] = \".agent/commands\"  # safe default, retry once\n        integration.setup(root, manifest, parsed_options=opts)\n    else:\n        raise","preventionTips":["Use relative commands_dir values without '..' or leading slashes.","Ensure no component of the destination is a symlink pointing outside the project.","Treat global/shared install locations as unsupported for commands_dir."],"tags":["path-traversal","security","options","integration-setup","spec-kit"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}