github/spec-kit · error · SystemExit

ERROR: SPECIFY_INIT_DIR is not a Spec Kit project (no .speci

Error message

ERROR: SPECIFY_INIT_DIR is not a Spec Kit project (no .specify/ directory): {init_root}

What it means

Final check of _ensure_safe_shared_destination(): dest exists, is not a symlink, but its resolved location falls outside the resolved project root. This catches hardlink-adjacent redirections and mount scenarios at the file level (a non-symlink path whose canonical location escapes), blocking the write before data is clobbered.

Source

Thrown at scripts/python/common.py:56

    except OSError:
        print(
            f"ERROR: SPECIFY_INIT_DIR does not point to an existing directory: {raw}",
            file=sys.stderr,
        )
        raise SystemExit(1)
    if not init_root.is_dir():
        print(
            f"ERROR: SPECIFY_INIT_DIR does not point to an existing directory: {raw}",
            file=sys.stderr,
        )
        raise SystemExit(1)
    if not (init_root / ".specify").is_dir():
        print(
            "ERROR: SPECIFY_INIT_DIR is not a Spec Kit project "
            f"(no .specify/ directory): {init_root}",
            file=sys.stderr,
        )
        raise SystemExit(1)
    return init_root


def get_repo_root(script_file: Path | None = None) -> Path:
    if os.environ.get("SPECIFY_INIT_DIR"):
        return resolve_specify_init_dir()

    specify_root = find_specify_root()
    if specify_root is not None:
        return specify_root

    if script_file is not None:
        script_root = find_specify_root(script_file.resolve().parent)
        if script_root is not None:
            return script_root

        # Installed scripts live at .specify/scripts/python/<script>.py.
        return script_file.resolve().parents[3]

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Pass project_path as Path(...).resolve()
  2. Replace mount/junction-backed destinations with real files on the project filesystem
  3. Move the working copy off redirecting temp/mount paths
  4. Verify with dest.resolve().relative_to(root) in a debug snippet to find the mismatch

Example fix

# before
write(proj, proj/'.specify'/'shared'/'x.yaml')  # x.yaml on a bind mount

# after
# remove mount, place real file, pass resolved root
write(Path(proj).resolve(), proj/'.specify'/'shared'/'x.yaml')
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

root = project_path.resolve()
if dest.exists():
    assert dest.resolve().is_relative_to(root), f'{dest} resolves outside project root'

Try / catch

try:
    _ensure_safe_shared_destination(project_path, dest)
except ValueError as e:
    if 'destination escapes project root' in str(e):
        _ensure_safe_shared_destination(Path(project_path).resolve(), dest)
    else:
        raise

Prevention

When it happens

Trigger: dest exists on a mount/junction whose resolve() leaves root; dest created via bind mount; root passed unresolved while dest canonicalizes differently (e.g. project under /tmp on macOS).

Common situations: FUSE/mount-backed project trees; WSL/devcontainer host mounts; moving a project between machines with redirecting paths; CI overlay filesystems.

Related errors


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