github/spec-kit · error · ValueError

Skills destination {new_skills_dir} contains a symlinked pat

Error message

Skills destination {new_skills_dir} contains a symlinked path component; refusing to install into it.

What it means

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.

Source

Thrown at src/specify_cli/integrations/kimi/__init__.py:103

        self,
        project_root: Path,
        manifest: IntegrationManifest,
        parsed_options: dict[str, Any] | None = None,
        **opts: Any,
    ) -> list[Path]:
        """Install skills with optional legacy migration."""
        parsed_options = parsed_options or {}

        # Refuse a symlinked destination before any writes occur. base
        # setup() only rejects a destination that *escapes* project_root
        # after resolve(), so an in-tree symlinked ``.kimi-code`` /
        # ``.kimi-code/skills`` (e.g. ``-> .``) would still pass that check
        # and misdirect the SKILL.md writes into an unintended in-tree
        # location (e.g. ``./skills/``). Reject any symlinked destination
        # component up front so this never happens.
        new_skills_dir = self.skills_dest(project_root)
        if _has_symlinked_component(new_skills_dir, project_root):
            raise ValueError(
                f"Skills destination {new_skills_dir} contains a symlinked "
                f"path component; refusing to install into it."
            )

        # Run base setup first so new-path targets (speckit-*) exist,
        # then migrate/clean legacy dirs without risking user content loss.
        created = super().setup(
            project_root, manifest, parsed_options=parsed_options, **opts
        )

        if parsed_options.get("migrate_legacy", False):
            old_skills_dir = project_root / ".kimi" / "skills"
            # Validate both endpoints. base setup() already rejects a
            # destination that *escapes* the project root, but an in-tree
            # symlinked ``.kimi-code``/``.kimi-code/skills`` (e.g. ``-> .``)
            # would still misdirect the move; ``_is_safe_legacy_dir`` rejects
            # any symlinked component, giving the destination the same
            # protection as the source.

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Delete the offending symlink(s) along the .kimi-code/skills path and recreate them as real directories.
  2. Verify with 'ls -la' that no component of the destination is a symlink.
  3. Re-run specify init / integration install for kimi.

Example fix

# before
ln -s . .kimi-code
specify integration install kimi

# after
rm .kimi-code
mkdir -p .kimi-code/skills
specify integration install kimi
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def has_symlinked_component(dest: Path, root: Path) -> bool:
    try:
        rel = dest.relative_to(root)
    except ValueError:
        return True
    cur = root
    for part in rel.parts:
        cur = cur / part
        if cur.is_symlink():
            return True
    return False

# before setup:
dest = integration.skills_dest(project_root)
if has_symlinked_component(dest, project_root):
    raise SystemExit(f"symlinked destination: {dest}")

Try / catch

try:
    integration.setup(project_root, manifest)
except ValueError as e:
    if "symlinked" in str(e):
        dest = integration.skills_dest(project_root)
        dest.unlink(missing_ok=True)
        dest.mkdir(parents=True, exist_ok=True)
        integration.setup(project_root, manifest)
    else:
        raise

Prevention

When it happens

Trigger: 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/.

Common situations: Dotfile-management setups symlinking tool directories; leftover experimental symlinks from earlier kimi integration versions; repository templates shipping a symlinked .kimi-code.

Related errors


AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14). Data as JSON: /api/errors/0b06907855e6076c. Report an issue: GitHub.