{"record":{"id":"754a402ec0e238b5","repo":"github/spec-kit","slug":"refusing-to-overwrite-symlinked-integration-manife","errorCode":null,"errorMessage":"Refusing to overwrite symlinked integration manifest path: {label}","messagePattern":"Refusing to overwrite symlinked integration manifest path: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/integrations/manifest.py","lineNumber":94,"sourceCode":"            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)\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    \"\"\"","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/integrations/manifest.py#L76-L112","documentation":"Raised by _ensure_safe_manifest_destination when the manifest file path itself (.specify/integrations/<key>.manifest.json) is a symlink. Overwriting a symlinked file would follow the link and could clobber a file outside the project, so the write is refused up front.","triggerScenarios":"IntegrationManifest.save() (or any write to manifest_path) when <key>.manifest.json is a symlink — commonly pointing at a shared manifest in a dotfiles repo or another worktree.","commonSituations":"Developers sharing manifests across worktrees via symlinks; dotfile managers (stow, chezmoi) that link config files; CI restoring cached manifests as links.","solutions":["Delete the symlink; save() will write a regular file (rm .specify/integrations/<key>.manifest.json)","If you need the file elsewhere, copy instead of symlink after the CLI writes it","Configure the dotfile manager to ignore .specify/integrations"],"exampleFix":"# before\nln -s ~/shared/claude.manifest.json .specify/integrations/claude.manifest.json\n# after\nrm .specify/integrations/claude.manifest.json  # save() recreates a regular file","handlingStrategy":"validation","validationCode":"mp = manifest.manifest_path\nif mp.is_symlink():\n    mp.unlink()  # let save() write a regular file","typeGuard":null,"tryCatchPattern":"try:\n    manifest.save()\nexcept ValueError as exc:\n    if \"symlinked integration manifest path\" in str(exc):\n        manifest.manifest_path.unlink()\n        manifest.save()\n    else:\n        raise","preventionTips":["Never symlink manifest JSON files across worktrees","Tell dotfile managers to exclude .specify/integrations","Copy shared manifests instead of linking them"],"tags":["manifest","symlink","security","integrations"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}