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

  1. Always construct IntegrationManifest with a resolved absolute project_root
  2. Recreate the manifest file as a plain file inside the repo (delete and re-run the install)
  3. 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

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


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