Surfaced while closing out #1619. Distinct from every issue currently open on that thread, and larger
than all of them.
The gap
#1619 and its fixes (#1676, #1679, #1688) are entirely about the ## Pre-computed context block —
!`cmd` lines the harness runs at skill load time. Removing $-expansion from those makes a
skill load under worktree isolation.
But the same guard applies at run time to every Bash call the skill's body instructs the model to
make. A skill can now load cleanly and then fail on its third step, when the body says to run
something like:
python3 "${CLAUDE_PLUGIN_ROOT}/skills/retro/scripts/parse-transcript.py" "$TRANSCRIPT"
Confirmed guard behavior on Bash-tool calls from inside a worktree-isolated agent:
| form |
result |
$HOME (bare, anywhere in the command) |
PASS |
any ${…} braced form, any name |
REFUSED |
$(…) command substitution |
REFUSED |
$CLAUDE_PLUGIN_ROOT, "$CLAUDE_PLUGIN_ROOT/x" |
REFUSED |
a bare $var local |
REFUSED ($FOO) |
Note the asymmetry that makes this specifically nasty: in pre-compute, ${CLAUDE_PLUGIN_ROOT} is
substituted by the harness into a literal path before any shell sees it, so it is safe. In a body
instruction the model copies the text into a Bash call, where it is $-text and is refused —
unless the harness substituted it during body rendering, which it does for the skill content itself.
Which of those two applies to a given body snippet has not been established, and it decides whether
this is a large problem or a small one. That is the first thing to determine.
Why it is not hypothetical
$(…) refusal was hit live during this work: an until [ "$(gh api …)" = … ] poll and a Monitor loop
both tripped the guard from an isolated agent. Any body instruction of the shape
BASE=$(git merge-base …) — a pattern this repo's own skills use, e.g. resolve-conflicts'
intent-recovery table — is refused there.
Scope — unknown, and that is the point
Nobody has swept skill bodies or reference/ spokes for $-bearing shell. The pre-compute sweep found
21 skills; the body surface is plausibly larger, since bodies carry most of the actual procedure.
Suggested first step is a sweep, not a fix:
- Grep
plugins/**/SKILL.md and plugins/**/reference/**.md for $-bearing shell outside
pre-compute blocks.
- Classify: harness-substituted plugin variable (safe) vs real shell expansion (refused under
isolation).
- Determine empirically whether a body-rendered
${CLAUDE_PLUGIN_ROOT} reaches the model as a literal
path. Evidence says yes for prose — a body reference rendered as
C:/Users/<user>/.claude/plugins/cache/melodic-software/claude-config/0.14.0/skills/… — but that
was one skill, and fenced code blocks may behave differently from prose.
- Only then decide the remedy.
Known workaround
The guard inspects the command string, not the script's contents. bash <literal-script-path>
passes, and the script may contain whatever shell it likes. So the general remedy for a refused body
command is to hoist the shell into a script file and invoke it by literal path — the same remedy
already proposed for the Class 2 pre-compute cases on #1687.
Caveats
- Everything above is the worktree-isolation guard as observed from inside an isolated agent.
Ordinary sessions are unaffected; this only bites isolated/parallel agents.
- The guard's mechanism is uncharacterized.
echo "$USER" is refused while echo "$USER/.claude"
passes, which no proposed model explains. Do not build a gate or a fix on an assumed mechanism —
test the specific forms.
Related
Surfaced while closing out #1619. Distinct from every issue currently open on that thread, and larger
than all of them.
The gap
#1619 and its fixes (#1676, #1679, #1688) are entirely about the
## Pre-computed contextblock —!`cmd`lines the harness runs at skill load time. Removing$-expansion from those makes askill load under worktree isolation.
But the same guard applies at run time to every Bash call the skill's body instructs the model to
make. A skill can now load cleanly and then fail on its third step, when the body says to run
something like:
Confirmed guard behavior on Bash-tool calls from inside a worktree-isolated agent:
$HOME(bare, anywhere in the command)${…}braced form, any name$(…)command substitution$CLAUDE_PLUGIN_ROOT,"$CLAUDE_PLUGIN_ROOT/x"$varlocal$FOO)Note the asymmetry that makes this specifically nasty: in pre-compute,
${CLAUDE_PLUGIN_ROOT}issubstituted by the harness into a literal path before any shell sees it, so it is safe. In a body
instruction the model copies the text into a Bash call, where it is
$-text and is refused —unless the harness substituted it during body rendering, which it does for the skill content itself.
Which of those two applies to a given body snippet has not been established, and it decides whether
this is a large problem or a small one. That is the first thing to determine.
Why it is not hypothetical
$(…)refusal was hit live during this work: anuntil [ "$(gh api …)" = … ]poll and a Monitor loopboth tripped the guard from an isolated agent. Any body instruction of the shape
BASE=$(git merge-base …)— a pattern this repo's own skills use, e.g.resolve-conflicts'intent-recovery table — is refused there.
Scope — unknown, and that is the point
Nobody has swept skill bodies or
reference/spokes for$-bearing shell. The pre-compute sweep found21 skills; the body surface is plausibly larger, since bodies carry most of the actual procedure.
Suggested first step is a sweep, not a fix:
plugins/**/SKILL.mdandplugins/**/reference/**.mdfor$-bearing shell outsidepre-compute blocks.
isolation).
${CLAUDE_PLUGIN_ROOT}reaches the model as a literalpath. Evidence says yes for prose — a body reference rendered as
C:/Users/<user>/.claude/plugins/cache/melodic-software/claude-config/0.14.0/skills/…— but thatwas one skill, and fenced code blocks may behave differently from prose.
Known workaround
The guard inspects the command string, not the script's contents.
bash <literal-script-path>passes, and the script may contain whatever shell it likes. So the general remedy for a refused body
command is to hoist the shell into a script file and invoke it by literal path — the same remedy
already proposed for the Class 2 pre-compute cases on #1687.
Caveats
Ordinary sessions are unaffected; this only bites isolated/parallel agents.
echo "$USER"is refused whileecho "$USER/.claude"passes, which no proposed model explains. Do not build a gate or a fix on an assumed mechanism —
test the specific forms.
Related
surface turns out to be real.