{"record":{"id":"2428ddfefee5c2f9","repo":"github/spec-kit","slug":"integration-manifest-path-escapes-project-root-l","errorCode":null,"errorMessage":"Integration manifest path escapes project root: {label}","messagePattern":"Integration manifest path escapes project root: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/integrations/manifest.py","lineNumber":101,"sourceCode":"            current.resolve().relative_to(root_resolved)\n        except (OSError, ValueError):\n            raise ValueError(f\"Integration manifest directory escapes project root: {label}\") from None\n\n\ndef _ensure_safe_manifest_destination(root: Path, path: Path) -> None:\n    \"\"\"Refuse manifest writes that would escape the project or follow symlinks.\"\"\"\n    root_resolved = root.resolve()\n    _ensure_safe_manifest_directory(root, path.parent)\n    label = _manifest_path_label(root, path)\n    if path.is_symlink():\n        raise ValueError(f\"Refusing to overwrite symlinked integration manifest path: {label}\")\n    if path.exists():\n        if not path.is_file():\n            raise ValueError(f\"Integration manifest path is not a file: {label}\")\n        try:\n            path.resolve().relative_to(root_resolved)\n        except (OSError, ValueError):\n            raise ValueError(f\"Integration manifest path escapes project root: {label}\") from None\n\n\nclass IntegrationManifest:\n    \"\"\"Tracks files installed by a single integration.\n\n    Parameters:\n        key:          Integration identifier (e.g. ``\"copilot\"``).\n        project_root: Absolute path to the project directory.\n        version:      CLI version string recorded in the manifest.\n        resolve_project_root: Resolve ``project_root`` before using it.\n    \"\"\"\n\n    def __init__(\n        self,\n        key: str,\n        project_root: Path,\n        version: str = \"\",\n        *,","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/integrations/manifest.py#L83-L119","documentation":"Final containment check when writing the manifest: if the existing manifest file path resolves outside the resolved project root, the write is refused. This catches hard-linked or otherwise aliased manifest files that would escape containment even without being symlinks.","triggerScenarios":"manifest_path exists and passes is_file() but path.resolve() is not under root_resolved — junctioned/hard-linked file, or a project_root passed unresolved while the file was created against the resolved location.","commonSituations":"Windows junctions or subst drives; project opened through a different mount point than where .specify was created; inconsistent resolved/unresolved root usage in custom scripts driving the CLI programmatically.","solutions":["Always construct IntegrationManifest with a resolved absolute project_root","Recreate the manifest file as a plain file inside the repo (delete and re-run the install)","Avoid junctions/mount aliases in the .specify path chain"],"exampleFix":"// before\nroot = Path(args.project)  # possibly a junction alias\n// after\nroot = Path(args.project).resolve()","handlingStrategy":"validation","validationCode":"root = Path(project_root).resolve()\nmp = root / \".specify\" / \"integrations\" / f\"{key}.manifest.json\"\ntry:\n    mp.resolve().relative_to(root)\nexcept (OSError, ValueError):\n    mp.unlink(missing_ok=True)  # alien file; let save() recreate","typeGuard":null,"tryCatchPattern":"try:\n    manifest.save()\nexcept ValueError as exc:\n    if \"escapes project root\" in str(exc):\n        replace_aliased_manifest_file()\n    else:\n        raise","preventionTips":["Consistently use resolved absolute project roots","Avoid hard links / junctions for manifest files","Open repos through one stable mount point"],"tags":["manifest","path-resolution","security","integrations"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}