abhigyanpatwari/GitNexus · error · ValueError

skill fingerprint input must be a regular non-symlink file:

Error message

skill fingerprint input must be a regular non-symlink file: {path}

What it means

skill_fingerprint walks `.claude/skills/<skill>` for the arm's skills and refuses any entry that is neither a directory nor a plain regular non-symlink file. lstat (not stat) is used so symlinks are detected as symlinks rather than followed. The guard keeps the evidence that feeds the promotion gate deterministic and prevents a crafted skill tree from redirecting the read.

Source

Thrown at eval/workflow_bench/evolution.py:470

    for skill_name in skill_names:
        _require_directory_chain(
            worktree,
            Path(".claude") / "skills" / skill_name,
            label="skill fingerprint root",
        )

    entries = sorted(
        (path for skill_name in skill_names for path in (worktree / ".claude" / "skills" / skill_name).rglob("*")),
        key=lambda path: path.relative_to(worktree).as_posix(),
    )
    files: list[Path] = []
    total = 0
    for path in entries:
        metadata = path.lstat()
        if stat.S_ISDIR(metadata.st_mode):
            continue
        if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISREG(metadata.st_mode):
            raise ValueError(f"skill fingerprint input must be a regular non-symlink file: {path}")
        total += metadata.st_size
        if total > MAX_SKILL_FINGERPRINT_BYTES:
            raise ValueError("skill fingerprint input exceeds the bounded evidence limit")
        files.append(path)
    return fingerprint_files(worktree, files)


def evaluate_candidate(
    results: dict[str, dict[str, dict[str, Any]]],
    *,
    incumbent_arm: str,
    candidate_arm: str,
    model: str | None,
    metric: str = "cost_usd",
    min_runs: int = 3,
    min_improvement_pct: float = 5.0,
    max_task_regression_pct: float = 20.0,
) -> dict[str, Any]:

View on GitHub (pinned to d540b00184)

Solutions

  1. Find offending entries: `find .claude/skills/<skill> -type l -o -type p -o -type s`.
  2. Replace symlinks with real copies: `cp -L --remove-destination <link> <link>`.
  3. Regenerate the skill packaging so it ships only regular files.

Example fix

# before
ln -s ../../shared/rules.md .claude/skills/gitnexus-plan/rules.md

# after
cp ../../shared/rules.md .claude/skills/gitnexus-plan/rules.md
Defensive patterns

Strategy: validation

Validate before calling

import stat, os
from pathlib import Path

def skill_files_are_regular(root: Path, skill_names: list[str]) -> list[Path]:
    out = []
    for name in skill_names:
        for p in (root / ".claude" / "skills" / name).rglob("*"):
            m = p.lstat().st_mode
            if stat.S_ISDIR(m):
                continue
            if stat.S_ISLNK(m) or not stat.S_ISREG(m):
                raise ValueError(f"non-regular skill file: {p}")
            out.append(p)
    return out

skill_files_are_regular(clone, skill_names)

Type guard

import stat
from pathlib import Path

def is_regular_non_symlink(path: Path) -> bool:
    m = path.lstat().st_mode
    return stat.S_ISREG(m) and not stat.S_ISLNK(m)

Prevention

When it happens

Trigger: A skill directory contains a symlink (e.g. a shared snippet `ln -s ../../shared/rules.md`), a broken symlink, or a special file (fifo/socket/device) created by accident or by a packaging step.

Common situations: Cross-skill shared content introduced via symlinks during local authoring; a vendored grammar or fixture symlinked into a skill dir; broken symlinks left after a move.

Related errors


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