Skill body is rewritten when arguments are passed: $1/$2 inside SKILL.md get replaced by words from the argument string

Status Open
Reported on v2.1.273
Maintainer reply None cached
Activity 2 comments · opened Sep 16, 2026

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:

  1. The index is off by one relative to shell semantics. With arguments ZULU YANKEE XRAY WHISKEY, the body's $1 became YANKEE — the second word. So the lookup appears to be a 0-based index into the split argument list, while $1 in every shell/awk context means the first parameter.
  2. Single-word arguments take a completely different path. With SOLO as the argument, the same body was injected verbatim (no substitution) and the argument was appended as a trailing ARGUMENTS: SOLO section 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 contained
awk -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 as
awk -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)

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗