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
- Set config['folder'] to a project-relative directory such as '.my-agent/'.
- Replace out-of-tree symlinks on the skills path with real directories.
- 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
- Keep config['folder'] project-relative and non-absolute.
- Make sure agent config/skills dirs in the project are real directories, not out-of-tree symlinks.
- Assert the resolved destination stays under the project root in integration unit tests.
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
- Integration destination {dest} escapes project root {project
- {type(self).__name__}.config is not set.
- {type(self).__name__}.config is missing required 'folder' en
- Error: --short-name requires a value
- Error: --number must be a non-negative integer
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/badc3309fa208116.
Report an issue: GitHub.