Skip to content

fix(skill-creator): isolate trigger-eval command files from the live project registry - #1261

Open
alvingarcia wants to merge 3 commits into
anthropics:mainfrom
alvingarcia:fix/skill-creator-eval-registry-isolation
Open

alvingarcia wants to merge 3 commits into
anthropics:mainfrom
alvingarcia:fix/skill-creator-eval-registry-isolation

Conversation

@alvingarcia

Copy link
Copy Markdown

Summary

Fixes #1260 — the trigger eval writes its synthetic {skill}-skill-{8hex}.md command files
into the user's live project .claude/commands/ (via find_project_root() + cwd=project_root),
so during the parallel eval window (default 10 workers) every concurrent Claude Code session
rooted at that project sees up to ~10 phantom hash-suffixed skill variants in its registry,
typeahead, and system-reminder skill listings.

This change isolates each eval variant in a per-query throwaway project root:

  • variant command file is written under tempfile.mkdtemp()/.claude/commands/
  • the claude -p subprocess runs with cwd=eval_root (it discovers commands from its cwd's
    .claude/; global auth/config in ~/.claude are unaffected by cwd)
  • finally removes the whole temp root (shutil.rmtree, replacing the per-file unlink)

The eval becomes hermetic: no live-registry pollution at any worker count, and project-level
commands/skills in the user's repo can no longer confound the trigger measurement. (This also
resolves the project-level case of anthropics/claude-plugins-official#632; skills installed
globally in ~/.claude/skills/ are still loaded regardless of cwd and would need a HOME
override — out of scope here.)

run_loop.py inherits the fix — it delegates to run_eval(). find_project_root() and the
project_root parameter are now unused for command placement; left intact to keep this diff
minimal, happy to remove them here or in a follow-up if preferred.

Test plan

  • python3 -m py_compile scripts/run_eval.py
  • Ran the patched eval from a directory inside a live Claude Code project (where the old
    code reproducibly wrote variants into the project's .claude/commands/):
    1-query eval set, --num-workers 1 --runs-per-query 1 → 1/1 PASS (variant discovered and
    triggered from the isolated root), the live project's .claude/commands/ was never created,
    and no skill-eval-* temp dirs were left behind.

🤖 Generated with Claude Code

alvingarcia and others added 2 commits June 4, 2026 17:28
…project registry

run_single_query wrote {skill}-skill-{8hex}.md into the nearest real
project's .claude/commands/ (find_project_root + cwd=project_root), so
during the parallel eval window every concurrent Claude Code session
rooted at that project saw the synthetic variants in its skill registry
and typeahead (up to num_workers at once, default 10).

Write each variant under a per-query tempfile.mkdtemp() root instead and
run claude -p with cwd=eval_root — commands are discovered from cwd's
.claude/, so the variant is visible to the eval subprocess only. The
finally now rmtrees the whole temp root (replacing the per-file unlink).

Fixes anthropics#1260

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ensics

A worker killed mid-query (SIGKILL) never reaches the rmtree, so a
stranded skill-eval-* tempdir should identify which skill/run left it.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@alvingarcia

Copy link
Copy Markdown
Author

Friendly ping on this one — it's been open ~3.5 weeks with no maintainer review. It's a small (+15/−8, single file), self-contained fix for the registry-pollution bug in #1260, with a green smoke test (variant discovered/triggered from the isolated root, live project .claude/commands/ never written, no temp dirs leaked). A community reviewer on #1260 also independently validated the per-query tempdir approach as sound.

Happy to fold in two optional refinements from that #1260 discussion if you'd like them in this PR:

  1. Prefix the eval root for forensics, so a crashed run is attributable:
    eval_root = Path(tempfile.mkdtemp(prefix=f"skilleval-{skill_name}-{unique_id}-"))
  2. Log the isolation so the hermetic-by-design intent is explicit in eval logs:
    logger.info(f"Isolated eval root: {eval_root} (cleaned on exit)")

I can also remove the now-unused find_project_root() / project_root parameter in this PR or a follow-up — whichever you prefer to keep the diff minimal. Just let me know the direction and I'll push.

Emit an info-level line when each trigger-eval variant is created under
its throwaway tempdir root, making the hermetic-by-design intent explicit
in eval logs. Uses a module logger (no handler configured), so it stays
silent by default and only surfaces when the caller enables logging.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@alvingarcia

Copy link
Copy Markdown
Author

Update: I've folded in both refinements from the #1260 discussion so the diff is final and ready to review.

  1. Forensic tempdir prefix — the eval root is now skill-eval-{skill_name}-{unique_id}-*, so a tempdir stranded by a SIGKILLed worker is attributable to a specific skill/run.
  2. Isolation log line (this push, 492a1f8) — a module-level logger.info("Isolated eval root: %s (cleaned on exit)", eval_root) makes the hermetic-by-design intent explicit in eval logs. It uses logging.getLogger(__name__) with no handler configured, so it's silent by default and only surfaces when the caller enables logging — no new stderr noise across the parallel workers.

Still a small, single-file change (run_eval.py), green smoke test unchanged (variant discovered/triggered from the isolated root; live .claude/commands/ never written; no temp dirs leaked).

I deliberately left the dead-code cleanup of find_project_root() / the project_root parameter out of this PR to keep the diff minimal, since run_loop.py also imports find_project_root and threads project_root through — removing it cleanly would touch that file too. Happy to do it as a small follow-up if you'd like the pass-through gone. Just let me know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant