Follow-up to #1619, which now carries the corrected diagnosis. This issue tracks the fix that
actually addresses the defect.
Why a new issue
#1676 merged a remediation for session-flow based on the wrong mechanism. It removed git
commands from seven skills' pre-compute blocks — but the trigger is a $-expansion, not git, and six
of those seven skills still carry !`echo "${CLAUDE_CODE_SESSION_ID:-unknown}" || echo "unknown"`.
They are still refused from a worktree-isolated agent at 0.17.15.
The work #1676 did is not wasted — moving git into individual body Bash calls is sound and those calls
demonstrably pass under isolation. It simply did not remove the blocker.
The rule (evidence on #1619)
A command is refused iff it contains a $-expansion in any form other than bare $HOME or
"$HOME". Git, pipes, 2>/dev/null, ||, and multi-line composition are irrelevant.
A skill is refused iff its pre-compute block contains at least one such $-expansion.
Controlled pair, both lines verbatim from real skills, run standalone under isolation:
git status --porcelain 2>/dev/null | head -20 || echo clean → PASS;
echo "${CLAUDE_CODE_SESSION_ID:-unknown}" || echo "unknown" → REFUSED.
Scope — 21 skills
| plugin |
skills |
the $ in question |
session-flow |
handoff, continue-in-background, orient, retro, running-retro, find-handoff |
${CLAUDE_CODE_SESSION_ID:-unknown}; find-handoff also ${HOME} |
source-control |
commit |
${CLAUDE_PLUGIN_ROOT}, $HOME, and two R="$(git rev-parse …)" one-liners |
claude-memory |
audit |
${CLAUDE_PLUGIN_ROOT}, $d |
claude-ops |
lanes, observability |
${CLAUDE_OPS_LANES_CONFIG:-…}, ${CC_OTEL_STORE:-…}, ${CLAUDE_PLUGIN_ROOT} |
code-tidying |
audit-comment-residue, tidy |
${CLAUDE_SKILL_DIR}, ${CLAUDE_PLUGIN_ROOT} |
docs-hygiene |
audit-noise, audit-derivability, compress |
${CLAUDE_SKILL_DIR}; note audit-derivability/compress hit it only via $NF-style awk/sed in a pipeline — confirm per file |
prototype |
explore-directions, pressure-test |
${CLAUDE_PLUGIN_ROOT} |
claude-config |
audit-automation-gaps |
${CLAUDE_PLUGIN_ROOT} |
education |
teach |
${CLAUDE_PROJECT_DIR}, ${CLAUDE_PLUGIN_DATA}, several locals |
firecrawl |
update |
${CLAUDE_SKILL_DIR} |
review |
fanout |
$D in a compound fallback |
Verify each file individually before editing — the sweep is a starting list, not a contract.
The fix
Per affected skill: remove the $-expansion from the pre-compute block. Either
- move the read into the skill body as an ordinary Bash call (the
#1676 pattern, which works), or
- emit the value without
${} where a literal will do.
${CLAUDE_PLUGIN_ROOT} cases need thought: that variable is how a plugin references its own bundled
scripts, and the plugins-reference documents it as the supported mechanism. Moving such a probe into
the body means the body must resolve the plugin root some other way, or the probe becomes a body-run
command that still needs the variable. This may not have a clean in-plugin answer, which would make
those skills genuinely harness-blocked rather than fixable here — decide that explicitly rather than
forcing a workaround.
Grouping
One PR per plugin (each owns its own plugin.json version and CHANGELOG.md): up to 11 PRs.
session-flow and source-control are the highest-traffic and should go first.
Verification — mandatory shape
Invoke from an Agent with isolation: "worktree":
- before: the skill is refused with the worktree-isolation guard message;
- after: it renders and runs;
- negative control (required): a skill predicted to STILL refuse. A green result without a control
is exactly what produced the original wrong diagnosis;
- positive control:
knowledge:course-digest (four complex pre-compute lines, no $, no git).
CI cannot prove any of this — it never invokes a skill from an isolated agent. The plugin cache is
version-keyed (~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/), so nothing is verifiable
until the fix ships and plugins are updated.
Open question worth settling first
Bare $HOME is the only expansion observed to pass; ${HOME} refuses. Whether that is a stable
allowlist or an artifact of one guard version is uncharacterized, and it decides whether $HOME is a
safe target form or should also be removed. Probe before relying on it.
Related
Follow-up to #1619, which now carries the corrected diagnosis. This issue tracks the fix that
actually addresses the defect.
Why a new issue
#1676 merged a remediation for
session-flowbased on the wrong mechanism. It removed gitcommands from seven skills' pre-compute blocks — but the trigger is a
$-expansion, not git, and sixof those seven skills still carry
!`echo "${CLAUDE_CODE_SESSION_ID:-unknown}" || echo "unknown"`.They are still refused from a worktree-isolated agent at
0.17.15.The work #1676 did is not wasted — moving git into individual body Bash calls is sound and those calls
demonstrably pass under isolation. It simply did not remove the blocker.
The rule (evidence on #1619)
Controlled pair, both lines verbatim from real skills, run standalone under isolation:
git status --porcelain 2>/dev/null | head -20 || echo clean→ PASS;echo "${CLAUDE_CODE_SESSION_ID:-unknown}" || echo "unknown"→ REFUSED.Scope — 21 skills
$in questionsession-flowhandoff,continue-in-background,orient,retro,running-retro,find-handoff${CLAUDE_CODE_SESSION_ID:-unknown};find-handoffalso${HOME}source-controlcommit${CLAUDE_PLUGIN_ROOT},$HOME, and twoR="$(git rev-parse …)"one-linersclaude-memoryaudit${CLAUDE_PLUGIN_ROOT},$dclaude-opslanes,observability${CLAUDE_OPS_LANES_CONFIG:-…},${CC_OTEL_STORE:-…},${CLAUDE_PLUGIN_ROOT}code-tidyingaudit-comment-residue,tidy${CLAUDE_SKILL_DIR},${CLAUDE_PLUGIN_ROOT}docs-hygieneaudit-noise,audit-derivability,compress${CLAUDE_SKILL_DIR}; noteaudit-derivability/compresshit it only via$NF-style awk/sed in a pipeline — confirm per fileprototypeexplore-directions,pressure-test${CLAUDE_PLUGIN_ROOT}claude-configaudit-automation-gaps${CLAUDE_PLUGIN_ROOT}educationteach${CLAUDE_PROJECT_DIR},${CLAUDE_PLUGIN_DATA}, several localsfirecrawlupdate${CLAUDE_SKILL_DIR}reviewfanout$Din a compound fallbackVerify each file individually before editing — the sweep is a starting list, not a contract.
The fix
Per affected skill: remove the
$-expansion from the pre-compute block. Either#1676pattern, which works), or${}where a literal will do.${CLAUDE_PLUGIN_ROOT}cases need thought: that variable is how a plugin references its own bundledscripts, and the plugins-reference documents it as the supported mechanism. Moving such a probe into
the body means the body must resolve the plugin root some other way, or the probe becomes a body-run
command that still needs the variable. This may not have a clean in-plugin answer, which would make
those skills genuinely harness-blocked rather than fixable here — decide that explicitly rather than
forcing a workaround.
Grouping
One PR per plugin (each owns its own
plugin.jsonversion andCHANGELOG.md): up to 11 PRs.session-flowandsource-controlare the highest-traffic and should go first.Verification — mandatory shape
Invoke from an
Agentwithisolation: "worktree":is exactly what produced the original wrong diagnosis;
knowledge:course-digest(four complex pre-compute lines, no$, no git).CI cannot prove any of this — it never invokes a skill from an isolated agent. The plugin cache is
version-keyed (
~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/), so nothing is verifiableuntil the fix ships and plugins are updated.
Open question worth settling first
Bare
$HOMEis the only expansion observed to pass;${HOME}refuses. Whether that is a stableallowlist or an artifact of one guard version is uncharacterized, and it decides whether
$HOMEis asafe target form or should also be removed. Probe before relying on it.
Related
$lines in place.source-controlgit migration does not removecommit's${CLAUDE_PLUGIN_ROOT}line.