{"record":{"id":"44ce6c6b6de87007","repo":"github/spec-kit","slug":"integration-manifest-directory-escapes-project-roo","errorCode":null,"errorMessage":"Integration manifest directory escapes project root: {label}","messagePattern":"Integration manifest directory escapes project root: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/integrations/manifest.py","lineNumber":65,"sourceCode":"        ) from None\n    return resolved\n\n\ndef _manifest_path_label(root: Path, path: Path) -> str:\n    try:\n        return path.relative_to(root).as_posix()\n    except ValueError:\n        return path.as_posix()\n\n\ndef _ensure_safe_manifest_directory(root: Path, directory: Path) -> None:\n    \"\"\"Create a manifest directory without following symlinked parents.\"\"\"\n    root_resolved = root.resolve()\n    try:\n        rel = directory.relative_to(root)\n    except ValueError:\n        label = _manifest_path_label(root, directory)\n        raise ValueError(f\"Integration manifest directory escapes project root: {label}\") from None\n\n    current = root\n    for part in rel.parts:\n        current = current / part\n        label = _manifest_path_label(root, current)\n        if current.is_symlink():\n            raise ValueError(f\"Refusing to use symlinked integration manifest directory: {label}\")\n        if current.exists():\n            if not current.is_dir():\n                raise ValueError(f\"Integration manifest directory path is not a directory: {label}\")\n            try:\n                current.resolve().relative_to(root_resolved)\n            except (OSError, ValueError):\n                raise ValueError(f\"Integration manifest directory escapes project root: {label}\") from None\n            continue\n        current.mkdir()\n        try:\n            current.resolve().relative_to(root_resolved)","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/integrations/manifest.py#L47-L83","documentation":"Raised by _ensure_safe_manifest_directory when the manifest directory itself is not even lexically relative to the project root (directory.relative_to(root) raises). This guards manifest writes (.specify/integrations/<key>.manifest.json) so the CLI never creates directories outside the project.","triggerScenarios":"Constructing an IntegrationManifest whose project_root does not prefix the computed manifest directory — e.g. project_root is a subdirectory while the manifest dir was computed from a parent, or root/path mismatch after moving or symlinking the repo.","commonSituations":"project_root passed as a relative or unresolved path that differs from the absolute manifest directory; tests using tmp_path fixtures with mismatched roots; repos where the working directory was changed between construction and save().","solutions":["Pass an absolute, resolved project_root (Path.cwd().resolve()) when constructing IntegrationManifest","Ensure the manifest directory is derived from the same root instance (use manifest.manifest_path rather than recomputing it)","In tests, derive expected paths from the same tmp_path object passed to the constructor"],"exampleFix":"// before\nmanifest = IntegrationManifest(\"claude\", Path(\".\"))\n// after\nmanifest = IntegrationManifest(\"claude\", Path.cwd().resolve())","handlingStrategy":"validation","validationCode":"root = Path(project_root).resolve()\nmanifest_dir = root / \".specify\" / \"integrations\"\ntry:\n    manifest_dir.relative_to(root)\nexcept ValueError:\n    raise SystemExit(f\"bad root: {manifest_dir} not under {root}\")","typeGuard":null,"tryCatchPattern":"try:\n    IntegrationManifest(key, root).save()\nexcept ValueError as exc:\n    if \"escapes project root\" in str(exc):\n        fix_root_and_retry()  # re-construct with resolved absolute root\n    else:\n        raise","preventionTips":["Always pass an absolute, .resolve()-d project_root","Derive the manifest dir from manifest.manifest_path instead of recomputing","In tests, reuse the tmp_path object for both root and assertions"],"tags":["manifest","path-validation","integrations"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}