{"record":{"id":"0b06907855e6076c","repo":"github/spec-kit","slug":"skills-destination-new-skills-dir-contains-a-sym","errorCode":null,"errorMessage":"Skills destination {new_skills_dir} contains a symlinked path component; refusing to install into it.","messagePattern":"Skills destination (.+?) contains a symlinked path component; refusing to install into it\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/integrations/kimi/__init__.py","lineNumber":103,"sourceCode":"        self,\n        project_root: Path,\n        manifest: IntegrationManifest,\n        parsed_options: dict[str, Any] | None = None,\n        **opts: Any,\n    ) -> list[Path]:\n        \"\"\"Install skills with optional legacy migration.\"\"\"\n        parsed_options = parsed_options or {}\n\n        # Refuse a symlinked destination before any writes occur. base\n        # setup() only rejects a destination that *escapes* project_root\n        # after resolve(), so an in-tree symlinked ``.kimi-code`` /\n        # ``.kimi-code/skills`` (e.g. ``-> .``) would still pass that check\n        # and misdirect the SKILL.md writes into an unintended in-tree\n        # location (e.g. ``./skills/``). Reject any symlinked destination\n        # component up front so this never happens.\n        new_skills_dir = self.skills_dest(project_root)\n        if _has_symlinked_component(new_skills_dir, project_root):\n            raise ValueError(\n                f\"Skills destination {new_skills_dir} contains a symlinked \"\n                f\"path component; refusing to install into it.\"\n            )\n\n        # Run base setup first so new-path targets (speckit-*) exist,\n        # then migrate/clean legacy dirs without risking user content loss.\n        created = super().setup(\n            project_root, manifest, parsed_options=parsed_options, **opts\n        )\n\n        if parsed_options.get(\"migrate_legacy\", False):\n            old_skills_dir = project_root / \".kimi\" / \"skills\"\n            # Validate both endpoints. base setup() already rejects a\n            # destination that *escapes* the project root, but an in-tree\n            # symlinked ``.kimi-code``/``.kimi-code/skills`` (e.g. ``-> .``)\n            # would still misdirect the move; ``_is_safe_legacy_dir`` rejects\n            # any symlinked component, giving the destination the same\n            # protection as the source.","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/integrations/kimi/__init__.py#L85-L121","documentation":"Raised by KimiIntegration.setup() (src/specify_cli/integrations/kimi/__init__.py:103) when the skills destination path (.kimi-code/skills by default) contains a symlinked component. The base setup only rejects destinations that escape project_root after resolve(); an in-tree symlink like .kimi-code -> . would pass that check yet redirect SKILL.md writes to an unintended location, so Kimi pre-checks every component with _has_symlinked_component() and refuses to install.","triggerScenarios":"Running kimi integration setup in a project where .kimi-code or .kimi-code/skills (or any parent component within the project) is a symlink — e.g. 'ln -s . .kimi-code' which would otherwise write into ./skills/.","commonSituations":"Dotfile-management setups symlinking tool directories; leftover experimental symlinks from earlier kimi integration versions; repository templates shipping a symlinked .kimi-code.","solutions":["Delete the offending symlink(s) along the .kimi-code/skills path and recreate them as real directories.","Verify with 'ls -la' that no component of the destination is a symlink.","Re-run specify init / integration install for kimi."],"exampleFix":"# before\nln -s . .kimi-code\nspecify integration install kimi\n\n# after\nrm .kimi-code\nmkdir -p .kimi-code/skills\nspecify integration install kimi","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef has_symlinked_component(dest: Path, root: Path) -> bool:\n    try:\n        rel = dest.relative_to(root)\n    except ValueError:\n        return True\n    cur = root\n    for part in rel.parts:\n        cur = cur / part\n        if cur.is_symlink():\n            return True\n    return False\n\n# before setup:\ndest = integration.skills_dest(project_root)\nif has_symlinked_component(dest, project_root):\n    raise SystemExit(f\"symlinked destination: {dest}\")","typeGuard":null,"tryCatchPattern":"try:\n    integration.setup(project_root, manifest)\nexcept ValueError as e:\n    if \"symlinked\" in str(e):\n        dest = integration.skills_dest(project_root)\n        dest.unlink(missing_ok=True)\n        dest.mkdir(parents=True, exist_ok=True)\n        integration.setup(project_root, manifest)\n    else:\n        raise","preventionTips":["Never symlink .kimi-code or .kimi-code/skills (dotfile managers often do — exclude these dirs).","Check 'ls -la' for symlinks along the destination path before install.","Recreate any symlayed directory as a real directory and re-run install."],"tags":["filesystem","symlink","security","integration-setup","spec-kit","kimi"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}