github/spec-kit · error · ValueError

Refusing to use symlinked integration manifest directory: {l

Error message

Refusing to use symlinked integration manifest directory: {label}

What it means

Raised while walking each component of the manifest directory path when a component is a symlink. The CLI refuses to follow symlinked directories because mkdir/resolve could then write or mutate state outside the project root, defeating the manifest's containment guarantees.

Source

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

    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)
        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()

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Remove the symlink and use a real directory (mkdir -p .specify/integrations)
  2. Point the symlink target inside the project, or bind-mount instead of symlinking
  3. If sharing manifests is the goal, copy them with a script instead of symlinking

Example fix

# before
ln -s ~/dotfiles/specify .specify
# after
mkdir -p .specify/integrations && cp ~/dotfiles/specify/*.json .specify/integrations/
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def manifest_dir_is_safe(root: Path, directory: Path) -> bool:
    try:
        rel = directory.relative_to(root)
    except ValueError:
        return False
    cur = root
    for part in rel.parts:
        cur = cur / part
        if cur.is_symlink():
            return False
    return True

Try / catch

try:
    manifest.save()
except ValueError as exc:
    if "symlinked integration manifest directory" in str(exc):
        replace_symlink_with_real_dir(label_from(exc))
    else:
        raise

Prevention

When it happens

Trigger: Any component of .specify, .specify/integrations, or deeper being a symlink when save() creates the manifest directory; typically .specify symlinked to a shared or external location.

Common situations: Developers symlinking .specify into a dotfiles repo or a shared config dir; CI caches that restore .specify as a symlink; containerized setups mounting config over symlinked paths.

Related errors


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