[BUG] Skill `args` silently rewrites literal dollar figures in SKILL.md prose — 22/27 skills exposed, 0/27 use argument placeholders
Summary
Invoking a skill via the Skill tool with a non-empty args string causes positional-argument substitution to be applied to the entire SKILL.md body, rewriting any literal $N token with the Nth whitespace-split word of args. Skills that contain dollar figures in ordinary prose are silently corrupted in-context while the file on disk stays clean.
This is filed as a standalone report on the installed-base angle, which the existing threads touch on but don't quantify. Related and partly overlapping: #79859 (closest match, I've added a confirmation there), #78759, #87109, #84212.
The distinguishing points here:
- Measured opt-in vs. exposure across a real production skill library.
- The greedy digit match makes the damage vary per invocation, so spot-checking one figure does not establish that a load was clean.
- The caller that supplies
argsis usually the model, not the user, so a skill author cannot predict whether their prose gets templated.
Environment
@anthropic-ai/claude-codev2.1.228- macOS 15.6.1, darwin arm64
- Plugin-provided skills (
~/.claude/plugins/cache/.../skills/<name>/SKILL.md)
How it surfaced
A skill documenting arts-funding program maxima was invoked by the model with a natural-language args string. Every category maximum in the delivered content was replaced by a word from that string: the funding ceilings the skill exists to state correctly all arrived wrong. The surrounding prose still read as authoritative, nothing flagged the substitution, and grep against the source file showed it was byte-clean. These figures get quoted to clients, and the skill in question exists specifically because a wrong maximum reached one once.
Reproduction (executed)
Same skill, same file on disk, invoked twice in one session.
Run A — no args. Every figure delivered verbatim. Clean.
Run B — args: "ZEBRAONE ZEBRATWO ZEBRATHREE ZEBRAFOUR ZEBRAFIVE ZEBRASIX ZEBRASEVEN ZEBRAEIGHT ZEBRANINE ZEBRATEN ZEBRAELEVEN"
| On disk | Delivered to model | Resolves to |
|---|---|---|
| $1,000 | ZEBRATWO,000 | ARGUMENTS[1] |
| $4,000 | ZEBRAFIVE,000 | ARGUMENTS[4] |
| $5,000 | ZEBRASIX,000 | ARGUMENTS[5] |
| $8,000 | ZEBRANINE,000 | ARGUMENTS[8] |
| $9,990 | ZEBRATEN,990 | ARGUMENTS[9] |
| $10,000 | ZEBRAELEVEN,000 | ARGUMENTS[10] |
| $15,000 | $15,000 | index 15 out of range, untouched |
| $500 | $500 | index 500 out of range, untouched |
Confirmed on a second, unrelated skill in the same session with args: "QQQZERO QQQONE QQQTWO QQQTHREE": $2,052 → QQQTWO,052 and $0.00 → QQQZERO.00, while $110 and $[OFFER AMOUNT] passed through untouched. The 0-indexed $N → $ARGUMENTS[N] mapping is documented behaviour; the defect is that it is applied to prose that declares no placeholders.
Equivalent minimal case
Not executed — a skill created mid-session isn't resolvable by the Skill tool until the session restarts, which is itself a small friction for reproducing this. Derived from the rule confirmed above:
---
name: repro-dollar
description: Repro for literal $N corruption
---
Grant maximum is $5,000.
Second tier is $1,000.
Full-length max is $15,000.
Web cap is $500.
Unit price is $0.50.
Invoke with args: "alpha bravo charlie delta echo foxtrot". Expected: all five lines verbatim. Predicted actual: foxtrot,000, bravo,000, $15,000, $500, alpha.50.
Why spot-checking doesn't catch it
The digit run is matched greedily, so which figures corrupt depends on the word count of args:
$1,000–$9,999corrupt with as few as 2–10 args.$10,000–$99,999need 11+ args.$500,$0.50-style short amounts and anything above the arg count survive.
A one-sentence args string is roughly ten words, which is exactly the band that destroys five-figure amounts while leaving four-figure ones intact. So the same skill corrupts differently on every invocation, and verifying one number tells you nothing about the others in the same load.
Prevalence: measured
Across a 27-skill production plugin used for grant consulting:
| Metric | Count |
|---|---|
| Skills containing $N,NNN-shaped literals reachable by this substitution | 22 / 27 |
| Skills using $ARGUMENTS, $ARGUMENTS[N], $N, or arguments: / argument-hint: frontmatter | 0 / 27 |
81% exposure, 0% opt-in. Not one skill in this library can consume an argument, so every args string ever passed to it has been pure downside: no placeholder received the value, and the only possible effect was rewriting a number. The per-skill figure counts run high in places (105, 79, 63, 61 literals), so a single bad invocation can corrupt dozens of numbers at once.
The library author never opted into templating, cannot tell from their own source that templating applies, and does not control whether args is passed, since the model chooses that at call time.
On the \$ escape
\$5,000 is documented at https://code.claude.com/docs/en/skills and does work. It is not a sufficient answer here:
- It protects only skills whose authors already know about this behaviour. Every skill written before that point stays exposed.
- It makes the safe way to write a dollar amount in prose the non-obvious one, in a file format that is otherwise plain Markdown.
- Remediating an existing library means escaping hundreds of literals across dozens of files to buy back a feature that library never used.
Suggested fix
Substitute args only into placeholders the author actually wrote. When a skill declares none, append the value inertly — which is already the behaviour for skills whose body happens to contain no $N match, so the safe path exists and is simply not taken when a literal collides.
If blanket substitution must stay for compatibility, gating it on the skill declaring arguments: / argument-hint: frontmatter would close the installed-base hole without changing the placeholder syntax for anyone using it deliberately.
A cheaper interim mitigation, if neither lands soon: emit a warning when args substitution actually replaces a token in a skill that declares no argument frontmatter. Right now the corruption's defining property is that it is completely silent.