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
- Find offending entries: `find .claude/skills/<skill> -type l -o -type p -o -type s`.
- Replace symlinks with real copies: `cp -L --remove-destination <link> <link>`.
- 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
- Author skills with copies, never symlinks: `cp` not `ln -s`.
- Audit shipped skill trees in CI: `find .claude/skills -type l` must be empty.
- Reject special files at packaging time so they never reach the fingerprint step.
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
- Compound Engineering plugin file must be regular and non-sym
- {label} must be a real non-symlink directory: {path}
- {label} must be a regular non-symlink file: {path}
- skill fingerprint input exceeds the bounded evidence limit
- evidence source must be a regular non-symlink file: {path}
AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12).
Data as JSON: /api/errors/06fd2762cfbae1d9.
Report an issue: GitHub.