github/spec-kit · error · ValueError

Refusing to scaffold through symlinked path: {label}

Error message

Refusing to scaffold through symlinked path: {label}

What it means

Raised while _assert_safe_scaffold_target walks each component of the target path relative to project_root: if any existing component (e.g. src, specify_cli, integrations, or the new package dir) is a symlink, scaffolding aborts. This blocks a symlink-based escape where writing 'inside' the tree actually lands in an attacker- or user-controlled location outside it. It mirrors the symlink-aware guarding used for integration manifests.

Source

Thrown at src/specify_cli/integration_scaffold.py:192

    Walks each component of *target* under *project_root* and rejects any
    existing symlinked directory (or symlinked target), then confirms the
    write destination still resolves inside the repository root. Mirrors the
    symlink-aware guarding used for integration manifests.
    """
    try:
        rel = target.relative_to(project_root)
    except ValueError:
        raise ValueError(
            f"Refusing to scaffold outside the repository root: {target}"
        ) from None

    current = project_root
    for part in rel.parts:
        current = current / part
        if current.is_symlink():
            label = current.relative_to(project_root).as_posix()
            raise ValueError(f"Refusing to scaffold through symlinked path: {label}")

    root_resolved = project_root.resolve()
    try:
        target.parent.resolve().relative_to(root_resolved)
    except (OSError, ValueError):
        raise ValueError(
            f"Refusing to scaffold outside the repository root: {target}"
        ) from None


def scaffold_integration(
    project_root: Path,
    key: str,
    integration_type: str,
) -> IntegrationScaffoldResult:
    """Create a minimal built-in integration package and test skeleton."""
    clean_key = _clean_key(key)
    normalized_type = integration_type.strip().lower()

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Replace the symlinked directory with a real directory (copy its contents in, remove the link): rm src/specify_cli/integrations && mv /somewhere/else integrations.
  2. If the link exists for workflow reasons, scaffold into a clean tree and copy the generated files over manually.
  3. Check with `find src tests -type l` from the repo root to locate the offending link named in the error message.

Example fix

# before: integrations is a symlink
$ ls -l src/specify_cli/integrations -> ../../shared/integrations
# after: real directory
$ rm src/specify_cli/integrations && cp -r ../../shared/integrations src/specify_cli/integrations
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def has_symlink_on_path(project_root: Path, target: Path) -> bool:
    rel = target.relative_to(project_root)
    cur = project_root
    for part in rel.parts:
        cur = cur / part
        if cur.is_symlink():
            return True
    return False

Try / catch

try:
    scaffold_integration(root, key, itype)
except ValueError as exc:
    if "symlinked path" in str(exc):
        print(f"remove the symlink named in: {exc}")  # then fix manually
    raise

Prevention

When it happens

Trigger: scaffold_integration target path traverses a directory that is a symlink, e.g. src/specify_cli/integrations -> /somewhere/else, or the target file itself already exists as a symlink; any component of rel.parts where current.is_symlink() is True.

Common situations: Developers symlinking the integrations directory from another checkout to share work; monorepo tooling that replaces source dirs with symlinks; a previously created package dir symlinked to a template dir.

Related errors


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