{"record":{"id":"9b3be0bd0d086b90","repo":"abhigyanpatwari/GitNexus","slug":"workspace-snapshot-root-must-be-a-real-directory","errorCode":null,"errorMessage":"workspace snapshot root must be a real directory: {root}","messagePattern":"workspace snapshot root must be a real directory: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"eval/workflow_bench/runner_artifacts.py","lineNumber":130,"sourceCode":"\ndef _is_bootstrap_noise(relative: PurePosixPath) -> bool:\n    \"\"\"Report whether a walked entry is harness noise rather than workspace change.\"\"\"\n\n    parts = relative.parts\n    if parts[0] == \".git\" or parts[0] in WORKSPACE_SNAPSHOT_BOOTSTRAP_NOISE:\n        return True\n    return len(parts) >= 2 and parts[-2] == CLAUDE_BOOTSTRAP_DIR and parts[-1] in CLAUDE_BOOTSTRAP_ENTRIES\n\n\ndef workspace_snapshot(worktree: Path) -> dict[str, str]:\n    \"\"\"Hash the workspace without following links, excluding Git internals\n    and Claude Code's own sandbox-bootstrap noise (see\n    WORKSPACE_SNAPSHOT_BOOTSTRAP_NOISE).\"\"\"\n\n    root = worktree.expanduser().absolute()\n    mode = root.lstat().st_mode\n    if stat.S_ISLNK(mode) or not stat.S_ISDIR(mode) or root.resolve(strict=True) != root:\n        raise ValueError(f\"workspace snapshot root must be a real directory: {root}\")\n\n    snapshot: dict[str, str] = {}\n    pending: list[tuple[Path, PurePosixPath]] = [(root, PurePosixPath())]\n    entry_count = 0\n    path_bytes = 0\n    file_bytes = 0\n    nofollow = getattr(os, \"O_NOFOLLOW\", 0)\n    while pending:\n        directory, relative_dir = pending.pop()\n        try:\n            children = sorted(os.scandir(directory), key=lambda entry: entry.name, reverse=True)\n        except OSError as exc:\n            raise ValueError(f\"workspace snapshot directory is unreadable: {directory}: {exc}\") from exc\n        for entry in children:\n            relative = relative_dir / entry.name\n            if _is_bootstrap_noise(relative):\n                continue\n            entry_count += 1","sourceCodeStart":112,"sourceCodeEnd":148,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/eval/workflow_bench/runner_artifacts.py#L112-L148","documentation":"Raised by workspace_snapshot() (runner_artifacts.py:130) when the worktree root is a symlink, not a directory, or is not resolve-stable (root.resolve(strict=True) != root). The snapshot walker refuses to follow links at the root so that a symlinked workspace cannot redirect hashing outside the benchmark boundary. This is the entry guard for the tamper-evident workspace diff used by phase-boundary checks.","triggerScenarios":"workspace_snapshot(worktree) is called with a worktree that lstat() reports as S_ISLNK, S_ISDIR false (e.g. a regular file or device node), or whose realpath differs from the absolute path (symlink loop, bind-mount whose resolution differs). Happens when make_worktree's clone target was replaced by a symlink, or the caller passed a ~/ symlinked path.","commonSituations":"User passes --repo ~/repos/project where ~/repos is a symlink to /mnt/repos; a prior failed run left a symlink where the worktree directory should be; running on macOS where /tmp is a symlink to /private/tmp and expanduser().absolute() does not resolve it.","solutions":["Pass the real (resolved) absolute path of the worktree: Path(...).resolve() before calling workspace_snapshot or before passing --repo.","Remove any symlink at the worktree root and re-create it as a real directory (make_worktree already does rmdir + clone, so ensure no leftover symlink).","On macOS, avoid /tmp; use a path under /Users/<user>/ or the project dir where realpath is stable."],"exampleFix":"# before\nroot = Path(\"~/repos/proj\").expanduser().absolute()  # may be a symlink\n\n# after\nroot = Path(\"~/repos/proj\").expanduser().resolve(strict=True)","handlingStrategy":"validation","validationCode":"import os, stat\nfrom pathlib import Path\n\nroot = Path(worktree).expanduser().resolve(strict=True)\nmode = root.lstat().st_mode\nassert not stat.S_ISLNK(mode), f\"root is a symlink: {root}\"\nassert stat.S_ISDIR(mode), f\"root is not a directory: {root}\"\n# resolve-stable: resolve(strict=True) must equal the absolute path\nassert root == Path(worktree).expanduser().absolute(), (\n    f\"root is not resolve-stable: {worktree} -> {root}\")\n# Pass `root` (resolved) to workspace_snapshot, not the original path.","typeGuard":"import os, stat\nfrom pathlib import Path\n\ndef is_real_directory(path: os.PathLike | str) -> bool:\n    p = Path(path).expanduser()\n    try:\n        st = p.lstat()\n    except OSError:\n        return False\n    if stat.S_ISLNK(st.st_mode) or not stat.S_ISDIR(st.st_mode):\n        return False\n    try:\n        return p.resolve(strict=True) == p.absolute()\n    except OSError:\n        return False","tryCatchPattern":null,"preventionTips":["Always pass Path(...).resolve() for worktree roots, especially under /tmp or ~.","On macOS, prefer paths under /Users or the project dir over /tmp (a symlink to /private/tmp).","Assert the root is a real directory before invoking the harness."],"tags":["filesystem","symlink","integrity","workspace-snapshot"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}