{"record":{"id":"e150d9ee32055d1d","repo":"github/spec-kit","slug":"error-specify-init-dir-is-not-a-spec-kit-project","errorCode":null,"errorMessage":"ERROR: SPECIFY_INIT_DIR is not a Spec Kit project (no .specify/ directory): {init_root}","messagePattern":"ERROR: SPECIFY_INIT_DIR is not a Spec Kit project \\(no \\.specify/ directory\\): (.+?)","errorType":"console","errorClass":"SystemExit","httpStatus":null,"severity":"error","filePath":"scripts/python/common.py","lineNumber":56,"sourceCode":"    except OSError:\n        print(\n            f\"ERROR: SPECIFY_INIT_DIR does not point to an existing directory: {raw}\",\n            file=sys.stderr,\n        )\n        raise SystemExit(1)\n    if not init_root.is_dir():\n        print(\n            f\"ERROR: SPECIFY_INIT_DIR does not point to an existing directory: {raw}\",\n            file=sys.stderr,\n        )\n        raise SystemExit(1)\n    if not (init_root / \".specify\").is_dir():\n        print(\n            \"ERROR: SPECIFY_INIT_DIR is not a Spec Kit project \"\n            f\"(no .specify/ directory): {init_root}\",\n            file=sys.stderr,\n        )\n        raise SystemExit(1)\n    return init_root\n\n\ndef get_repo_root(script_file: Path | None = None) -> Path:\n    if os.environ.get(\"SPECIFY_INIT_DIR\"):\n        return resolve_specify_init_dir()\n\n    specify_root = find_specify_root()\n    if specify_root is not None:\n        return specify_root\n\n    if script_file is not None:\n        script_root = find_specify_root(script_file.resolve().parent)\n        if script_root is not None:\n            return script_root\n\n        # Installed scripts live at .specify/scripts/python/<script>.py.\n        return script_file.resolve().parents[3]","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/scripts/python/common.py#L38-L74","documentation":"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.","triggerScenarios":"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).","commonSituations":"FUSE/mount-backed project trees; WSL/devcontainer host mounts; moving a project between machines with redirecting paths; CI overlay filesystems.","solutions":["Pass project_path as Path(...).resolve()","Replace mount/junction-backed destinations with real files on the project filesystem","Move the working copy off redirecting temp/mount paths","Verify with dest.resolve().relative_to(root) in a debug snippet to find the mismatch"],"exampleFix":"# before\nwrite(proj, proj/'.specify'/'shared'/'x.yaml')  # x.yaml on a bind mount\n\n# after\n# remove mount, place real file, pass resolved root\nwrite(Path(proj).resolve(), proj/'.specify'/'shared'/'x.yaml')","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\nroot = project_path.resolve()\nif dest.exists():\n    assert dest.resolve().is_relative_to(root), f'{dest} resolves outside project root'","typeGuard":null,"tryCatchPattern":"try:\n    _ensure_safe_shared_destination(project_path, dest)\nexcept ValueError as e:\n    if 'destination escapes project root' in str(e):\n        _ensure_safe_shared_destination(Path(project_path).resolve(), dest)\n    else:\n        raise","preventionTips":["Keep .specify destinations on the same native filesystem as the project","Avoid mount/junction-backed files at destination paths","Pass resolved project roots to shared-infra APIs"],"tags":["filesystem","symlink","security","portability"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}