Audience: operators whose run printed a skill-shadow warning or whose
plugin-shadow.jsonorbenchmark.jsonreports a live copy of an eval skill.
Use this guide after eval-magic reports a discoverable live skill. It explains how to exclude that
source from eval-agent dispatches and how to verify the result. Normal task-repository isolation and
the write guard are separate concerns documented by eval-magic run --help.
An eval compares the same task with and without a skill, or with two revisions. If the harness can discover another copy of the subject skill in both arms, the control arm is not skill-free and the delta is invalid.
Sibling collisions have two outcomes:
- A sibling visible in both arms of every group is a warning because the comparison remains symmetric but differs from a clean environment.
- A sibling visible in only one arm is comparison-invalid because its effect cannot be separated from the skill under test.
The preflight reports two source classes in schema-v3 plugin-shadow.json:
operator-environment— global skills, enabled plugins, and other sources inherited from the machine running eval-magic.codebase-sourced— matching project-local skills preserved from the task codebase.
Transcript evidence can later show what a dispatch loaded. Eval-magic does not parse shell templates to infer that a flag or environment variable isolates the process.
Apply the remedy to every eval-agent command, including every resumed turn of a scripted eval. Isolating only the first round allows the live copy to return on the next round. Judge commands do not need isolation because they receive a rubric and transcript rather than the task itself.
Choose one remedy:
| Remedy | Hides | Leaves visible | Caveat |
|---|---|---|---|
--setting-sources project,local |
User-scope plugins and ~/.claude/skills |
Project and local settings, including staged skills and the guard | Authentication is unchanged. |
Disable one entry in enabledPlugins |
The named plugin | Other plugins and global skills | The setting must be in a source loaded by the dispatch. |
A fresh CLAUDE_CONFIG_DIR |
Installed plugins and global skills | Project-local staged content | OAuth state may not follow the relocated directory; use an API key or authenticate it. |
--setting-sources project,local is the usual first choice because it changes one dispatch without
editing global configuration. Project-local staged skills still load under every remedy.
Choose the remedy that matches each reported source:
| Remedy | Hides | Leaves visible | Caveat |
|---|---|---|---|
codex --disable plugins ... exec |
Skills from enabled plugins | Repository, user, and admin skill directories | --disable plugins is global and must appear before exec. |
| Move or rename the conflicting directory | That direct skill source | Every other source | Required for repository and admin skills. |
A clean HOME |
$HOME/.agents/skills |
CODEX_HOME plugins and repository/admin skills |
Preserve CODEX_HOME when the dispatch needs the installed configuration. |
Codex also has bundled system skills without a stable enumeration surface. Check possible name collisions with them separately.
OpenCode can load skills installed for other harnesses, including ~/.claude/skills and
~/.agents/skills.
| Remedy | Hides | Leaves visible |
|---|---|---|
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS=1 |
Global and project .claude roots |
.agents and .opencode roots |
OPENCODE_DISABLE_EXTERNAL_SKILLS=1 |
.claude and .agents roots |
.opencode roots |
| Move or rename the conflicting directory | That direct source | Every other source |
The preflight cannot enumerate config-declared skills.paths, remote skills.urls, or the singular
.opencode/skill/ directory. Check those sources when the project uses them.
After excluding every reported source from every initial and resumed eval-agent command, record the result in a descriptor layer:
label = "claude-code"
[shadow]
isolates_live_sources = trueThe declaration covers only operator-environment findings. It does not claim that skills sourced
from the task codebase are isolated. Use codebase.exclude_skill_sources: true for that separate
policy when project skills should not participate; see eval-magic docs codebase.
The declaration does not disable detection. plugin-shadow.json retains every source and its
intrinsic severity as provenance. run presents operator-environment findings as informational,
and aggregate omits those warnings only while no transcript evidence contradicts the declaration.
Codebase-sourced findings remain warnings regardless of this descriptor setting.
The declaration is written from the descriptor resolved at prep time, so the remedy must ride along
with every follow-up. When the descriptor arrives via --harness-file, the generated RUNBOOK.md
and Next: commands re-emit the flag — follow them verbatim. Dropping it silently reverts to the
un-overlaid descriptor while the declaration stands, the comparison-invalid state this guide exists
to prevent. dispatch and ingest compare the resolved descriptor against the digest run
records in conditions.json and warn when the two differ.
Do not set it when:
- Any reported source remains discoverable.
- A scripted resume command lacks the remedy.
- Codex uses
--disable pluginsbut the report also names a direct skill directory. - OpenCode uses an external-skill switch but the report names an
.opencodesource. - You have not verified every rendered eval-agent command.
Partial isolation does not qualify.
From the iteration directory, read plugin-shadow.json after ingest:
jq -r '.findings[] | "\(.skill_name): \(.resolved_severity // "not verified")"' \
plugin-shadow.jsonThe outcomes mean:
isolated: every expected cell reported a roster and none loaded the source.comparison-invalidorwarning: a dispatch loaded the source or the evidence could not settle the finding.- No
resolved_severity: the harness cannot report a roster or ingest has not run.
Refuting a finding requires evidence from every expected cell. One confirmed load is enough to keep the warning. A missing transcript never proves isolation.
Claude Code stream JSON includes a session-opening {"type":"system","subtype":"init"} record.
From one task's outputs/ directory, inspect it directly:
jq 'select(.subtype == "init") | {plugins, skills}' claude-events.jsonlAn empty array is positive evidence that no entries of that type loaded. Check the specific runtime
ID rather than the total list length because staged and bundled skills remain present. Scripted runs
produce an init record under each turn-N/; inspect a resumed turn as well.
Codex and OpenCode captures do not provide the equivalent roster used by eval-magic. Verify those
harnesses by checking the eval-agent command dispatch-manifest.md says the runner will spawn,
then use isolates_live_sources to record the operator assertion.
claude plugin list reports installed plugins, not what one dispatch loaded. It does not accept the
dispatch's setting-source selection. A plugin can appear there and remain absent from the dispatch,
or the reverse. Use the dispatch's init event.
skill_name in the evals/evals.json file accepts either one skill name or an ordered,
non-empty list:
{
"skill_name": ["review-workflow", "review-verification"],
"evals": [
{
"id": "review-change",
"prompt": "Review this change.",
"expected_output": "A prioritized review."
}
]
}With a list, --skill selects the eval owner: the member whose evals/ directory supplies the
definitions and overlay files, and whose name owns the workspace and promotion destination. The
owner must appear in the list. --stage-name is unavailable because one override cannot name
several staged skills.
Every treatment member is copied into the eval home before any dispatch runs, and each condition
stages from those copies. Mode A stages all treatment members in with_skill and none in
without_skill. Other siblings from --skill-dir remain ambient in both arms. Mode B snapshots
and stages the complete set in both revisions. A scalar skill_name retains the existing
single-skill paths and artifacts.
Each copy is the working tree as it sits on disk, not a checkout of a commit. Evaluating an
uncommitted revision is the ordinary case, and in a --mode revision run the edit under test is
uncommitted by definition. What the run measured is recorded rather than inferred in
conditions.json, each run.json, benchmark.json, and the BASELINE.md file written by
promote-baseline:
jq '.skill_source' conditions.jsondirty: true means the recorded revision alone does not identify what ran. Commit the treatment
skills before a run whose result you intend to publish.
The copy freezes the treatment, not the assertions. grade reads assertions and
skill_should_trigger from the live evals/evals.json and everything else from the copy, because
the judging loop authors assertions from the run's own evidence, after the dispatch they grade.
Each grading.json records which file supplied its assertions. See eval-magic docs judging.
Ambient skills staged by --skill-dir are copied the same way, and the roster is captured once
when the run resolves. For a multi-skill treatment, skill_source.eval_owner names the owner and
skill_source.skills records every treatment member's resolved source and revision. The
siblings field, when present, names ambient skills staged in both arms.
The eval home sits outside the skill's own repository: under $XDG_DATA_HOME/eval-magic
(or ~/.local/share/eval-magic), in a directory named for the skill directory it serves.
run prints the path it chose, and every command it suggests carries --workspace-dir,
so there is nothing to remember. EVAL_MAGIC_WORKSPACE_DIR moves the default;
--workspace-dir overrides both.
Copying does not remove the live directories from the machine, so a dispatch can still read one by
absolute path. detect-stray-writes checks every treatment source and reports that as a live-source
read. aggregate carries it into validity_warnings for the same reason a discoverable plugin
copy is carried there: the arm may not be comparing what it claims to.
Skill-source isolation is about what a dispatch can load. The task repository is about what it can
reach: every dispatch runs in its own private environment, a Git repository with no remotes and
hooks disabled, marked with refs/eval-magic/baseline at the state the agent started from. That
codebase-backed boundary holds for every eval; task-specific files are overlays on the sourced
tree. See eval-magic docs codebase.
The two are independent. An environment can be a faithfully isolated repository while the dispatch still loads a live skill source, and a shadowed skill is not made safe by the repository boundary.
Do not declare isolation. Retain the validity warning as the record of a known threat. A symmetric sibling collision can still support a qualified comparison, but a subject collision or asymmetric sibling collision requires another run after the source is excluded.