github/spec-kit · error · ValueError
Integration manifest path escapes project root: {label}
Error message
Integration manifest path escapes project root: {label} What it means
Final containment check when writing the manifest: if the existing manifest file path resolves outside the resolved project root, the write is refused. This catches hard-linked or otherwise aliased manifest files that would escape containment even without being symlinks.
Source
Thrown at src/specify_cli/integrations/manifest.py:101
current.resolve().relative_to(root_resolved)
except (OSError, ValueError):
raise ValueError(f"Integration manifest directory escapes project root: {label}") from None
def _ensure_safe_manifest_destination(root: Path, path: Path) -> None:
"""Refuse manifest writes that would escape the project or follow symlinks."""
root_resolved = root.resolve()
_ensure_safe_manifest_directory(root, path.parent)
label = _manifest_path_label(root, path)
if path.is_symlink():
raise ValueError(f"Refusing to overwrite symlinked integration manifest path: {label}")
if path.exists():
if not path.is_file():
raise ValueError(f"Integration manifest path is not a file: {label}")
try:
path.resolve().relative_to(root_resolved)
except (OSError, ValueError):
raise ValueError(f"Integration manifest path escapes project root: {label}") from None
class IntegrationManifest:
"""Tracks files installed by a single integration.
Parameters:
key: Integration identifier (e.g. ``"copilot"``).
project_root: Absolute path to the project directory.
version: CLI version string recorded in the manifest.
resolve_project_root: Resolve ``project_root`` before using it.
"""
def __init__(
self,
key: str,
project_root: Path,
version: str = "",
*,View on GitHub (pinned to bf88c9f9a8)
Solutions
- Always construct IntegrationManifest with a resolved absolute project_root
- Recreate the manifest file as a plain file inside the repo (delete and re-run the install)
- Avoid junctions/mount aliases in the .specify path chain
Example fix
// before root = Path(args.project) # possibly a junction alias // after root = Path(args.project).resolve()
Defensive patterns
Strategy: validation
Validate before calling
root = Path(project_root).resolve()
mp = root / ".specify" / "integrations" / f"{key}.manifest.json"
try:
mp.resolve().relative_to(root)
except (OSError, ValueError):
mp.unlink(missing_ok=True) # alien file; let save() recreate Try / catch
try:
manifest.save()
except ValueError as exc:
if "escapes project root" in str(exc):
replace_aliased_manifest_file()
else:
raise Prevention
- Consistently use resolved absolute project roots
- Avoid hard links / junctions for manifest files
- Open repos through one stable mount point
When it happens
Trigger: manifest_path exists and passes is_file() but path.resolve() is not under root_resolved — junctioned/hard-linked file, or a project_root passed unresolved while the file was created against the resolved location.
Common situations: Windows junctions or subst drives; project opened through a different mount point than where .specify was created; inconsistent resolved/unresolved root usage in custom scripts driving the CLI programmatically.
Related errors
- Path {rel} resolves to {resolved} which is outside the proje
- Refusing to use symlinked integration manifest directory: {l
- Refusing to overwrite symlinked integration manifest path: {
- Invalid command 'file' {label}: {reason}
- Invalid alias {alias!r} for command '{cmd['name']}': {alias_
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/2428ddfefee5c2f9.
Report an issue: GitHub.