Skill/command argument substitution is off by one and limited to $0/$1
Summary
When a skill or command is invoked with arguments, the loader substitutes $0 and $1 in the injected body with args[0] and args[1] — 0-indexed. Per the documented convention $1 should be the first argument, so placeholders are off by one. $2–$9 are never substituted at all.
Two consequences:
- Commands read the wrong argument.
$1yields argument 2;$2yields nothing and is passed through as the literal text$2. - Literal
$0/$1text in prose is silently corrupted. Any$followed by0or1and then a non-digit — dollar amounts like$1.42B,$1M,$0,$1,000,000— is replaced by a word from the user's prompt. No error; the number just becomes a plausible-looking token.
(2) is the more serious half: it is silent, and it corrupts documentation content that has nothing to do with parameters.
Environment
- Claude Code 2.1.259, desktop app (Code tab), model Opus 5
- Windows 11 Enterprise 10.0.26100
- Plugin skills loaded from
~/.claude/plugins/cache/
Expected vs actual
Skill invoked with arguments ALPHA BRAVO CHARLIE:
| Placeholder | Expected | Actual |
|---|---|---|
| $1 | ALPHA | BRAVO |
| $2 | BRAVO | $2 (literal) |
| $3 | CHARLIE | $3 (literal) |
| $0 | (unsubstituted) | ALPHA |
Reproduction
A skill body containing:
index0=$0 index1=$1 index2=$2 index5=$5 amount=$1.42B literal=$500k
invoked with ALPHA BRAVO CHARLIE yields:
index0=ALPHA index1=BRAVO index2=$2 index5=$5 amount=BRAVO.42B literal=$500k
Two things to note. $1.42B → BRAVO.42B: substitution does not require $N to be a standalone token, so it reaches into ordinary prose. $500k survives only because the character after $5 is another digit.
(Placeholder names and amounts throughout this report; the behaviour was observed across five invocations of a real plugin on 2.1.259.)
Observed pattern
| Body contains | Args | Received |
|---|---|---|
| ~$1.42B | ALPHA BRAVO CHARLIE | ~BRAVO.42B |
| adjustment = $0 | same | adjustment = ALPHA |
| = "Less than $1" | same | = "Less than BRAVO" |
| ~$500k/month (~$6M/yr) | same | unchanged |
| LIMIT_$2M, ±$5K | same | unchanged |
| script.py $1 $2 | AAA BBB | script.py BBB $2 |
The last row is a user-visible failure. A command that shells out to a wrapped script passed the second argument where the script expected the first, plus a literal $2, and the script errored out on the wrong input. The plugin markdown was written correctly against the documented convention.
Related observation
When the injected body contains no $0/$1, the loader appends a trailing ARGUMENTS: <raw string> line instead. When substitution does occur, no trailer is appended. Self-consistent, but it means a body mixing both — a real $1 parameter plus a literal dollar amount — silently consumes the arguments and corrupts the prose.
Impact
Any plugin following the documented $1/$2 convention. That includes the first-party databricks plugin in anthropics/claude-plugins-official, where commands/doctor.md:21 and commands/setup.md:21 both use $1 (static read of the shipped files; not invoked to confirm).
For content corruption the exposure is any $ followed by 0 or 1 then a non-digit. In one plugin audited for this, that was 16 occurrences across 6 skill files, including monetary targets and two literal values that mirror strings stored in a source system (e.g. a column whose stored value is the string Less than $1) and so cannot be reworded without making the documentation wrong.
Workaround
Authors can dodge the content corruption by writing USD 1.42B instead of $1.42B. For parameters, $ARGUMENTS plus manual parsing is the usual advice, though per #46922 inline $ARGUMENTS substitution appears to have been dropped — in practice the appended ARGUMENTS: trailer still makes the raw string visible to the model.
Neither workaround helps where the $1 is a literal value in a source system; those need an escape mechanism.
Suggested fix
- Index positional parameters from 1, so
$1is the first argument. - Support
$1–$9, not just$0/$1. - Provide an escape (
\$1or$$1) for literal dollar amounts and stored string values. - Consider restricting substitution to standalone tokens, so
$1.42Bin prose is not a substitution site.
Possibly related
- #23585 — forked-skill
$1/$2not resolving at all (closed as completed). Different failure: here they resolve, but to the wrong index. - #46922 —
$ARGUMENTSsubstitution in SKILL.md (closed as not planned).
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗