github/spec-kit · error · ValueError

Path {rel} resolves to {resolved} which is outside the proje

Error message

Path {rel} resolves to {resolved} which is outside the project root {root_resolved}

What it means

Raised by _validate_rel_path in the integration manifest layer when a manifest-relative path, after resolving (root / rel), no longer sits under the resolved project root. The manifest may only track files inside the project so uninstall stays non-destructive. This is the generic escape error, distinct from the absolute-path and '..'-canonicality errors raised nearby.

Source

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

    return h.hexdigest()


def _validate_rel_path(rel: Path, root: Path) -> Path:
    """Resolve *rel* against *root* and verify it stays within *root*.

    Raises ``ValueError`` if *rel* is absolute, contains ``..`` segments
    that escape *root*, or otherwise resolves outside the project root.
    """
    if rel.is_absolute():
        raise ValueError(
            f"Absolute paths are not allowed in manifests: {rel}"
        )
    resolved = (root / rel).resolve()
    root_resolved = root.resolve()
    try:
        resolved.relative_to(root_resolved)
    except ValueError:
        raise ValueError(
            f"Path {rel} resolves to {resolved} which is outside "
            f"the project root {root_resolved}"
        ) 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)

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Use a plain canonical relative path with no '..' components (e.g. '.claude/commands/speckit.build.md')
  2. If the path was built by joining paths, normalize it first with Path(os.path.normpath(p)) and verify it stays relative to root
  3. Check for symlinked ancestors along the path and replace the symlink with a real directory or move the target inside the project
  4. Pass the same resolved project_root that the IntegrationManifest instance was constructed with

Example fix

// before
manifest.record_file("../shared/commands/build.md")
// after
manifest.record_file("shared/commands/build.md")  # move/copy the file inside project root
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def safe_rel(root: Path, rel: str | Path) -> Path | None:
    p = Path(rel)
    if p.is_absolute() or ".." in p.parts:
        return None
    root_r = root.resolve()
    try:
        resolved = (root / p).resolve()
        resolved.relative_to(root_r)
    except (OSError, ValueError):
        return None
    if any((root / part).is_symlink() for part in _accum(p)):
        return None
    return resolved

Type guard

def is_canonical_rel_path(rel: str) -> bool:
    p = pathlib.PurePosixPath(rel)
    return not p.is_absolute() and ".." not in p.parts and not rel.startswith("~")

Try / catch

try:
    manifest.record_file(rel)
except ValueError as exc:
    if "outside the project root" in str(exc):
        log.warning("skipping out-of-project path %s", rel)
    else:
        raise

Prevention

When it happens

Trigger: Calling IntegrationManifest.record_file()/record_existing()/check_modified()/uninstall() with a rel_path containing '..' segments that escape root (e.g. '../../etc/passwd'), or a path whose intermediate directory is a symlink pointing outside the project, so resolve() lands outside root_resolved.

Common situations: Hand-built rel_path joining a template dir with '..' segments; a project dir containing symlinked subdirectories (monorepo shared folders, dotfile managers); passing a path computed against a different (non-resolved) project_root.

Related errors


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