Skip to content

skills: $-bearing shell in skill BODIES and reference spokes is refused under worktree isolation at run time (untracked surface, larger than #1619's pre-compute scope) #1691

Description

@kyle-sexton

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:

  1. Grep plugins/**/SKILL.md and plugins/**/reference/**.md for $-bearing shell outside
    pre-compute blocks.
  2. Classify: harness-substituted plugin variable (safe) vs real shell expansion (refused under
    isolation).
  3. 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.
  4. 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

Activity

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

Metadata

Metadata

Assignees

Labels

needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.priority: mediumReal value, no hard deadline; normal backlog flow.status: needs-decisionAwaiting a human or maintainer judgment call.work-class: read-onlyAudits, research, reports. No repository mutation; tracker and queue writes only.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions