{"record":{"id":"355f22a3cfe67b67","repo":"github/spec-kit","slug":"integration-manifest-path-is-not-a-file-label","errorCode":null,"errorMessage":"Integration manifest path is not a file: {label}","messagePattern":"Integration manifest path is not a file: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/integrations/manifest.py","lineNumber":97,"sourceCode":"                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)\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,","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/integrations/manifest.py#L79-L115","documentation":"Raised when the destination manifest path exists but is not a regular file (e.g. it is a directory, FIFO, or device). The manifest writer only replaces regular files and refuses anything else.","triggerScenarios":"A directory named <key>.manifest.json exists under .specify/integrations when save() runs — usually the residue of a bad copy, a template that rendered a directory where a file belongs, or an interrupted install that left a partial tree.","commonSituations":"Restoring from a backup that dropped the trailing filename and created a directory; rsync/cp mistakes; concurrent or half-failed installs.","solutions":["Check the path from the error message: ls -la .specify/integrations/<key>.manifest.json","If it is a directory, remove it (rm -rf) and re-run the specify command","If it holds wanted data, move it aside first"],"exampleFix":"# before\n.specify/integrations/claude.manifest.json/   # a directory\n# after\nrm -rf .specify/integrations/claude.manifest.json  # then re-run install","handlingStrategy":"validation","validationCode":"mp = manifest.manifest_path\nassert not (mp.exists() and not mp.is_file()), f\"non-file at {mp}\"","typeGuard":null,"tryCatchPattern":"try:\n    manifest.save()\nexcept ValueError as exc:\n    if \"not a file\" in str(exc):\n        shutil.rmtree(manifest.manifest_path, ignore_errors=True)\n        manifest.save()\n    else:\n        raise","preventionTips":["Audit restored backups for directories where files belong","Verify with ls -la after restore scripts","Avoid cp/rsync inversions when moving .specify around"],"tags":["manifest","filesystem","integrations"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}