github/spec-kit · error · ValueError

Skills destination {skills_dir} escapes project root {projec

Error message

Skills destination {skills_dir} escapes project root {project_root_resolved}

What it means

The skills setup()'s second guard: after the manifest root matches, it resolves skills_dest() (config folder + commands_subdir, e.g. .claude/skills or .agents/skills) and requires it inside the resolved project root. It blocks skills installation from escaping the project via a misconfigured folder, an absolute path, or an out-of-tree symlink on the destination directory.

Source

Thrown at src/specify_cli/integrations/base.py:1706

        ``name``, ``description``, ``compatibility``, and ``metadata``.
        """

        templates = self.list_command_templates()
        if not templates:
            return []

        project_root_resolved = project_root.resolve()
        if manifest.project_root != project_root_resolved:
            raise ValueError(
                f"manifest.project_root ({manifest.project_root}) does not match "
                f"project_root ({project_root_resolved})"
            )

        skills_dir = self.skills_dest(project_root).resolve()
        try:
            skills_dir.relative_to(project_root_resolved)
        except ValueError as exc:
            raise ValueError(
                f"Skills destination {skills_dir} escapes "
                f"project root {project_root_resolved}"
            ) from exc

        script_type = opts.get("script_type", "sh")
        arg_placeholder = (
            self.registrar_config.get("args", "$ARGUMENTS")
            if self.registrar_config
            else "$ARGUMENTS"
        )
        created: list[Path] = []

        for src_file in templates:
            raw = src_file.read_text(encoding="utf-8")

            # Derive the skill name from the template stem
            command_name = src_file.stem  # e.g. "plan"
            skill_name = f"speckit-{command_name.replace('.', '-')}"

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Set config['folder'] to a project-relative directory such as '.my-agent/'.
  2. Replace out-of-tree symlinks on the skills path with real directories.
  3. Verify the resolved destination: run Path(skills_dest).resolve() and confirm it is under the project root.

Example fix

# before
class MyIntegration(SkillsIntegration):
    config = {"folder": "~/.claude-shared", ...}
# after
class MyIntegration(SkillsIntegration):
    config = {"folder": ".claude", ...}
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def skills_dest_safe(project_root: Path, integration) -> bool:
    try:
        integration.skills_dest(project_root).resolve().relative_to(project_root.resolve())
    except ValueError:
        return False
    return True

Prevention

When it happens

Trigger: config['folder'] absolute or containing '..' in a custom skills integration; the skills directory (e.g. .claude) symlinked to a location outside the project.

Common situations: Custom skills integrations with hard-coded folders; users symlinking agent config dirs to dotfiles repos; forked integrations where folder was never localized.

Related errors


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