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
- 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.
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
- 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.
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
- Integration destination {dest_resolved} escapes project root
- Integration destination {dest} escapes project root {project
- Error: --short-name requires a value
- Error: --number must be a non-negative integer
- Error: branch_template must not place {slug} before {number}
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/0b06907855e6076c.
Report an issue: GitHub.