abhigyanpatwari/GitNexus · error · ValueError

{phase} changed the evaluated skill fingerprint

Error message

{phase} changed the evaluated skill fingerprint

What it means

Raised by require_skill_fingerprint (runner_artifacts.py:249) when skill_fingerprint(worktree, arm) itself raises OSError or ValueError while re-hashing the evaluated skill roots after a bounded phase. The wrapper converts any underlying read/parse failure into a single 'phase changed the evaluated skill fingerprint' error, failing closed: the harness cannot confirm the skill surface is unchanged, so it treats the phase as having tampered with it.

Source

Thrown at eval/workflow_bench/runner_artifacts.py:249

    while parent.parts:
        parent_key = parent.as_posix()
        if parent_key not in before and after.get(parent_key, "").startswith("d:"):
            allowed.add(parent_key)
        parent = parent.parent
    unauthorized = sorted(changed - allowed)
    if unauthorized:
        preview = ", ".join(unauthorized[:8])
        suffix = " …" if len(unauthorized) > 8 else ""
        raise ValueError(f"phase changed unauthorized workspace path(s): {preview}{suffix}")


def require_skill_fingerprint(worktree: Path, arm: str, expected: str | None, *, phase: str) -> None:
    """Fail closed when a bounded phase changes the evaluated prompt roots."""

    try:
        observed = skill_fingerprint(worktree, arm)
    except (OSError, ValueError) as exc:
        raise ValueError(f"{phase} changed the evaluated skill fingerprint") from exc
    if observed != expected:
        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()

View on GitHub (pinned to d540b00184)

Solutions

  1. Unwrap the chained exception (__cause__) to see the original OSError/ValueError and the offending path; the original message names the file or directory.
  2. Restore the skill root from git (git -C <worktree> checkout -- .claude/skills) and re-run the phase with a prompt that forbids editing .claude/skills.
  3. If a large legitimate file triggered the size limit, move it out of the skill root before the phase.

Example fix

# before: phase writes a symlink into the skill root
ln -s /etc/passwd .claude/skills/gitnexus-plan/SKILL.md

# after: phase leaves skill roots untouched; restore if disturbed
git -C worktree checkout -- .claude/skills
Defensive patterns

Strategy: try-catch

Validate before calling

from pathlib import Path
from eval.workflow_bench.evolution import skill_fingerprint

# Before the phase, confirm skill_fingerprint can be computed without error.
root = Path(worktree).resolve()
fp = skill_fingerprint(root, arm)
assert fp is not None, f"arm {arm} has no evaluated skills"
# After the phase, recompute; any exception here is what 475 wraps.
fp2 = skill_fingerprint(root, arm)
assert fp == fp2, "skill surface changed during phase preview"

Try / catch

try:
    require_skill_fingerprint(worktree, arm, expected, phase="work")
except ValueError as exc:
    # Unwrap the cause to find the offending path/type error
    cause = exc.__cause__
    log.error("skill fingerprint failure: %s", cause)
    # Restore skill roots from git before any retry
    subprocess.run(["git", "-C", str(worktree), "checkout", "--", ".claude/skills"], check=True)
    raise

Prevention

When it happens

Trigger: skill_fingerprint() at evolution.py:440 walks .claude/skills/<skill>/ and encounters a missing directory (_require_directory_chain raises), a symlink/non-regular file inside the skill root (raises ValueError at line 470), or an OSError reading a file. The phase under inspection ran between the before/after fingerprint and disturbed the skill tree.

Common situations: A planning/work phase (or a setup step) writes a symlink into .claude/skills/, deletes a skill directory, changes permissions so a file is unreadable, or exceeds MAX_SKILL_FINGERPRINT_BYTES by writing a large file into the skill root.

Related errors


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