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
- Unwrap the chained exception (__cause__) to see the original OSError/ValueError and the offending path; the original message names the file or directory.
- 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.
- 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
- Mount .claude/skills read-only for bounded phases.
- Forbid the model from editing .claude/skills in the phase prompt.
- Restore skill roots from git before retrying after a failure.
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
- phase artifact escapes the workspace: {allowed_artifact}
- phase artifact must be a regular non-symlink file: {relative
- phase changed unauthorized workspace path(s): {preview}{suff
- Analyzer build or dependency runtime changed during analysis
- Search index build failed with an integrity error and the an
AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12).
Data as JSON: /api/errors/140476bf24bc83ed.
Report an issue: GitHub.