github/spec-kit · error · ValueError

Integration manifest directory escapes project root: {label}

Error message

Integration manifest directory escapes project root: {label}

What it means

Raised by _ensure_safe_manifest_directory when the manifest directory itself is not even lexically relative to the project root (directory.relative_to(root) raises). This guards manifest writes (.specify/integrations/<key>.manifest.json) so the CLI never creates directories outside the project.

Source

Thrown at src/specify_cli/integrations/manifest.py:65

        ) from None
    return resolved


def _manifest_path_label(root: Path, path: Path) -> str:
    try:
        return path.relative_to(root).as_posix()
    except ValueError:
        return path.as_posix()


def _ensure_safe_manifest_directory(root: Path, directory: Path) -> None:
    """Create a manifest directory without following symlinked parents."""
    root_resolved = root.resolve()
    try:
        rel = directory.relative_to(root)
    except ValueError:
        label = _manifest_path_label(root, directory)
        raise ValueError(f"Integration manifest directory escapes project root: {label}") from None

    current = root
    for part in rel.parts:
        current = current / part
        label = _manifest_path_label(root, current)
        if current.is_symlink():
            raise ValueError(f"Refusing to use symlinked integration manifest directory: {label}")
        if current.exists():
            if not current.is_dir():
                raise ValueError(f"Integration manifest directory path is not a directory: {label}")
            try:
                current.resolve().relative_to(root_resolved)
            except (OSError, ValueError):
                raise ValueError(f"Integration manifest directory escapes project root: {label}") from None
            continue
        current.mkdir()
        try:
            current.resolve().relative_to(root_resolved)

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Pass an absolute, resolved project_root (Path.cwd().resolve()) when constructing IntegrationManifest
  2. Ensure the manifest directory is derived from the same root instance (use manifest.manifest_path rather than recomputing it)
  3. In tests, derive expected paths from the same tmp_path object passed to the constructor

Example fix

// before
manifest = IntegrationManifest("claude", Path("."))
// after
manifest = IntegrationManifest("claude", Path.cwd().resolve())
Defensive patterns

Strategy: validation

Validate before calling

root = Path(project_root).resolve()
manifest_dir = root / ".specify" / "integrations"
try:
    manifest_dir.relative_to(root)
except ValueError:
    raise SystemExit(f"bad root: {manifest_dir} not under {root}")

Try / catch

try:
    IntegrationManifest(key, root).save()
except ValueError as exc:
    if "escapes project root" in str(exc):
        fix_root_and_retry()  # re-construct with resolved absolute root
    else:
        raise

Prevention

When it happens

Trigger: Constructing an IntegrationManifest whose project_root does not prefix the computed manifest directory — e.g. project_root is a subdirectory while the manifest dir was computed from a parent, or root/path mismatch after moving or symlinking the repo.

Common situations: project_root passed as a relative or unresolved path that differs from the absolute manifest directory; tests using tmp_path fixtures with mismatched roots; repos where the working directory was changed between construction and save().

Related errors


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