Skip to content

fix(skills): pre-compute Class 2 remainder — genuine shell expansion mixed into pre-compute in 5 plugins (canonical tracker; scope in comments, body list superseded) #1687

Description

@kyle-sexton

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 cleanPASS;
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

Activity

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

Metadata

Metadata

Assignees

Labels

priority: highSignificant impact, or blocks an imminent release; staff this cycle.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions