abhigyanpatwari/GitNexus · error · ValueError

plan artifact cannot be a symlink: {path}

Error message

plan artifact cannot be a symlink: {path}

What it means

Raised by snapshot_plan_docs (runner_artifacts.py:269) when a .md/.html file under docs/plans has S_ISLNK true in its lstat. Plan artifacts must be regular files so their content hash is trustworthy; a symlink could redirect the hash to arbitrary content (e.g. /etc/passwd) and is rejected before opening.

Source

Thrown at eval/workflow_bench/runner_artifacts.py:269

        raise ValueError(f"{phase} changed the evaluated skill fingerprint")


def snapshot_plan_docs(worktree: Path) -> dict[Path, str]:
    """Hash direct, regular plan artifacts without following links."""

    plans = worktree / "docs" / "plans"
    if not plans.exists():
        return {}
    if plans.is_symlink() or not plans.is_dir():
        raise ValueError(f"plan directory must be a real directory: {plans}")

    snapshot: dict[Path, str] = {}
    for path in sorted(plans.iterdir()):
        if path.suffix.lower() not in {".md", ".html"}:
            continue
        metadata = path.lstat()
        if stat.S_ISLNK(metadata.st_mode):
            raise ValueError(f"plan artifact cannot be a symlink: {path}")
        if not stat.S_ISREG(metadata.st_mode):
            raise ValueError(f"plan artifact must be a regular file: {path}")
        descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
        try:
            opened = os.fstat(descriptor)
            if not stat.S_ISREG(opened.st_mode) or opened.st_dev != metadata.st_dev or opened.st_ino != metadata.st_ino:
                raise ValueError(f"plan artifact changed while opening: {path}")
            with os.fdopen(descriptor, "rb", closefd=False) as handle:
                snapshot[path] = hashlib.file_digest(handle, "sha256").hexdigest()
            after = os.fstat(descriptor)
            if (opened.st_size, opened.st_mtime_ns) != (after.st_size, after.st_mtime_ns):
                raise ValueError(f"plan artifact changed while hashing: {path}")
        finally:
            os.close(descriptor)
    return snapshot


def new_plan_doc(worktree: Path, before: dict[Path, str]) -> Path:

View on GitHub (pinned to d540b00184)

Solutions

  1. Find the symlink under docs/plans (find docs/plans -type l) and replace it with a real file (cp -L then rm the link).
  2. Constrain the planning prompt to write regular files only; forbid symlinks in docs/plans.
  3. If the repo intentionally versioned symlinks, restructure to commit real files.

Example fix

# before
ln -s ../templates/plan.md docs/plans/plan.md

# after
cp ../templates/plan.md docs/plans/plan.md
Defensive patterns

Strategy: validation

Validate before calling

import stat
from pathlib import Path

plans = Path(worktree) / "docs" / "plans"
if plans.is_dir():
    for p in plans.iterdir():
        if p.suffix.lower() in {".md", ".html"}:
            assert not stat.S_ISLNK(p.lstat().st_mode), f"plan is a symlink: {p}"
            assert stat.S_ISREG(p.lstat().st_mode), f"plan is not regular: {p}"

Type guard

import stat
from pathlib import Path

def all_plans_are_regular_files(worktree) -> bool:
    plans = Path(worktree) / "docs" / "plans"
    if not plans.is_dir():
        return True
    for p in plans.iterdir():
        if p.suffix.lower() not in {".md", ".html"}:
            continue
        mode = p.lstat().st_mode
        if stat.S_ISLNK(mode) or not stat.S_ISREG(mode):
            return False
    return True

Prevention

When it happens

Trigger: For a .md/.html entry in docs/plans, path.lstat().st_mode is S_ISLNK. The model or a setup step replaced a plan file with a symlink, or the repo commits symlinks into docs/plans.

Common situations: A planning phase symlinks docs/plans/foo.md to an existing template; a repo keeps canonical plans in a sibling dir and symlinks them in; a model tries to 'reuse' another plan via a symlink.

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12). Data as JSON: /api/errors/d1ce40c7adcf82cd. Report an issue: GitHub.