{"record":{"id":"4d144cca928ec3fd","repo":"abhigyanpatwari/GitNexus","slug":"repository-root-must-not-traverse-symlinks-root","errorCode":null,"errorMessage":"repository root must not traverse symlinks: {root}","messagePattern":"repository root must not traverse symlinks: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"eval/workflow_bench/promotion_apply.py","lineNumber":160,"sourceCode":"        stat.S_ISLNK(final.st_mode)\n        or not stat.S_ISREG(final.st_mode)\n        or not (identity(before) == identity(opened) == identity(after) == identity(final))\n    ):\n        raise ValueError(f\"overlay destination changed while being read: {target}\")\n    return b\"\".join(chunks), opened.st_mode\n\n\ndef _open_repository_root(repo_root: Path) -> tuple[Path, int]:\n    root = repo_root.expanduser().absolute()\n    try:\n        metadata = root.lstat()\n        resolved = root.resolve(strict=True)\n    except OSError as exc:\n        raise ValueError(f\"repository root is unavailable: {root}\") from exc\n    if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode):\n        raise ValueError(f\"repository root must be a real directory: {root}\")\n    if resolved != root:\n        raise ValueError(f\"repository root must not traverse symlinks: {root}\")\n    flags = os.O_RDONLY | os.O_DIRECTORY | getattr(os, \"O_CLOEXEC\", 0) | getattr(os, \"O_NOFOLLOW\", 0)\n    try:\n        descriptor = os.open(root, flags)\n    except OSError as exc:\n        raise ValueError(f\"repository root changed while opening: {root}\") from exc\n    try:\n        opened = os.fstat(descriptor)\n        final = root.lstat()\n        final_resolved = root.resolve(strict=True)\n\n        def identity(value: os.stat_result) -> tuple[int, int, int]:\n            return value.st_dev, value.st_ino, stat.S_IFMT(value.st_mode)\n\n        if (\n            stat.S_ISLNK(final.st_mode)\n            or not stat.S_ISDIR(opened.st_mode)\n            or not stat.S_ISDIR(final.st_mode)\n            or final_resolved != root","sourceCodeStart":142,"sourceCodeEnd":178,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/eval/workflow_bench/promotion_apply.py#L142-L178","documentation":"All errors below are raised by internal helpers of `eval/workflow_bench/promotion_apply.py` and propagate to the caller of the public entry points: `apply_promoted_overlay(overlay, repo_root, *, expected_digest, expected_target_bases)`, `destination_base_digests(overlay, repo_root)`, `committed_destination_base_digests(overlay, repo_root, *, ref)` and `freeze_overlay(overlay, destination)`. The module applies promoted skill overlays across the canonical skill tree plus its shipped mirrors (`gitnexus/skills`, `gitnexus-claude-plugin/skills`) in a TOCTOU-hardened, symlink-rejecting, descriptor-bound transaction. `_open_repository_root` computes `resolved = root.resolve(strict=True)` and compares it to the (already `.expanduser().absolute()`-d) `root`. If they differ, some lexical component of the path traverses a symlink, and the guard fires. Even if the final entry is a real dir, a symlinked intermediate component is rejected.","triggerScenarios":"Any path component of `repo_root` is a symlink, so `resolve()` yields a different string than the lexical absolute path (e.g. `/tmp` -> `/private/tmp` on macOS, or a symlinked parent like `/var/tmp`).","commonSituations":"macOS where `/tmp` resolves to `/private/tmp`; home dir under a symlinked `/home`; XDG cache under a symlinked `/mnt/cache`; promotion run from a path whose parent is a symlink for layout convenience.","solutions":["Print `Path(root).resolve(strict=True)` vs `Path(root).absolute()` and find the differing component.","Rewrite the path using the resolved (non-symlink) components, or remove the offending symlink in the path chain.","On macOS, prefer `/private/tmp` over `/tmp`; elsewhere expand all symlinks in the parent chain."],"exampleFix":"// before\nroot = Path('/tmp/work/repo')  # /tmp -> /private/tmp on macOS\napply_promoted_overlay(overlay, repo_root=root)\n// after\nroot = Path('/tmp/work/repo').resolve(strict=True)\n# ensure no component is itself a symlink by using the resolved form\napply_promoted_overlay(overlay, repo_root=root)","handlingStrategy":"validation","validationCode":"def lexical_real_root(p):\n    root = Path(p).expanduser().absolute()\n    if root.resolve(strict=True) != root:\n        raise ValueError(f'repo_root traverses a symlink; use the resolved path: {root.resolve(strict=True)}')\n    return root","typeGuard":"def root_is_lexical_non_symlinked(p: str) -> bool:\n    root = Path(p).expanduser().absolute()\n    try:\n        return root.resolve(strict=True) == root\n    except OSError:\n        return False","tryCatchPattern":"except ValueError as exc:\n    if 'must not traverse symlinks' in str(exc):\n        resolved = Path(repo_root).resolve(strict=True)\n        log.warning('re-running promotion against symlink-free root %s', resolved)\n        apply_promoted_overlay(overlay, repo_root=resolved)","preventionTips":["Pass the fully-resolved (`Path.resolve(strict=True)`) checkout path.","Avoid running promotion from under OS-managed symlinked temp/cache dirs.","In CI, use a freshly cloned real path under a non-symlinked work prefix."],"tags":["filesystem","security","symlink","repository-root","validation"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}