Skill body is rewritten when arguments are passed: $1/$2 inside SKILL.md get replaced by words from the argument string
What's Wrong?
When a skill is invoked with arguments, the SKILL.md body that gets injected into the conversation is rewritten: any $<digit> token in the body is replaced by a word taken from the argument string. The skill's own instructions are silently altered before I ever read them.
This matters because $1 / $2 are extremely common inside awk, sed and shell snippets in skill bodies. After substitution the command is still syntactically valid, so it fails silently and produces a wrong number rather than an error.
Two further details:
- The index is off by one relative to shell semantics. With arguments
ZULU YANKEE XRAY WHISKEY, the body's$1becameYANKEE— the second word. So the lookup appears to be a 0-based index into the split argument list, while$1in every shell/awk context means the first parameter. - Single-word arguments take a completely different path. With
SOLOas the argument, the same body was injected verbatim (no substitution) and the argument was appended as a trailingARGUMENTS: SOLOsection instead. So whether the body is rewritten depends on the argument string, not on the skill.
Real-world impact I hit before isolating this: a skill's body containedawk -F'\t' -v n="$NOISE" '$3 !~ n {s+=$1+$2} END {print s+0}' to sum added+deleted lines from git diff --numstat. With a multi-word argument it arrived asawk -F'\t' -v n="$NOISE" '$3 !~ n {s+=<some words from my argument>} END {print s+0}'. $3 and $NOISE were untouched; only $1 and $2 were replaced. That expression still parses, so it just yields a wrong line count — which in that skill selected the review tier and how many subagents to dispatch. I only noticed because the substituted words happened to be readable Chinese text.
What Should Happen?
The SKILL.md body should be injected verbatim, exactly as it is on disk. Arguments should be passed alongside it (the trailing ARGUMENTS: section that single-word arguments already produce looks like the intended mechanism), never by find-and-replace inside the body.
If positional substitution is a deliberate feature, it should at minimum be opt-in per skill rather than applied to every $<digit> occurrence, since shell code in skill bodies cannot be distinguished from placeholders by pattern alone.
Error Messages/Logs
None — this is a silent corruption. No warning is emitted and the resulting commands remain syntactically valid.
Steps to Reproduce
Any skill whose body contains a $<digit> token works. I used the vendored requesting-code-review skill from the public obra/superpowers project, whose SKILL.md contains this line verbatim:
BASE_SHA=$(git log --oneline | grep "Task 1" | head -1 | awk '{print $1}')
Run 1 — multi-word argument:
Invoke the skill with arguments ZULU YANKEE XRAY WHISKEY. The injected body shows:
BASE_SHA=$(git log --oneline | grep "Task 1" | head -1 | awk '{print YANKEE}')
$1 → YANKEE (the second word). No trailing ARGUMENTS: section is present. Note that $(git rev-parse HEAD) on the following line and head -1 in the same pipeline are both untouched, so the rewrite specifically targets $<digit>.
Run 2 — single-word argument:
Invoke the same skill with argument SOLO. The injected body shows the line unchanged:
BASE_SHA=$(git log --oneline | grep "Task 1" | head -1 | awk '{print $1}')
and a trailing ARGUMENTS: SOLO section is appended instead.
Same skill, same file on disk, two different injected bodies.
Equivalent minimal skill (written out for convenience; I did not run this one separately, my observations above are from the requesting-code-review body):
---
name: dollar-repro
description: Minimal repro for positional substitution in skill bodies
---
Line A: awk '{print $1}'
Line B: awk '{print $2}'
Line C: awk '{s+=$1+$2} END {print s+0}'
Line D: untouched controls: $(git rev-parse HEAD), $HOME, head -1
Invoke it once with ZULU YANKEE XRAY WHISKEY and once with SOLO, and compare lines A–C in the injected body.
Claude Model
Opus 5 (claude-opus-5[1m])
Is this a regression?
Not sure — I have not tested earlier versions.
Claude Code Version
2.1.273 (Claude Code)
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗