DEV Community

Rulestack
Rulestack

Posted on

Claude Code skill arguments across 12 runs: what $ARGUMENTS, $0 and argument-hint actually put in the body

Across 12 claude -p runs on Claude Code 2.1.278, $ARGUMENTS always carried the raw string exactly as typed (quotes included), $0/$1/$2 were split shell-style with quotes stripped, a missing positional stayed as the literal text $2, and argument-hint changed nothing in what the skill body received. One surprise: a bare $HOME or $ARGUMENTS token typed as an argument vanished from the positional slots while surviving in $ARGUMENTS.

We run a small shop on Claude Code and most of our repeatable work lives in skills that get invoked as slash commands with arguments. When a skill misbehaves, the first question is always the same: what did the body actually receive after substitution? The documentation describes the rules, but I wanted to see the substituted text with my own eyes rather than trust either the docs or the model's paraphrase. So I built four throwaway skills whose only job is to echo their arguments, ran them 12 times under claude -p, and read the substituted body straight out of the session transcript. This article is the record of those runs.

Everything below was done on 2026-09-22 with Claude Code 2.1.278 (claude --version). The documentation quotes come from https://code.claude.com/docs/en/skills fetched the same day with trafilatura. A small aside on that: https://code.claude.com/docs/en/slash-commands returned a byte-identical page (same MD5, title "Extend Claude with skills - Claude Code Docs"), so as of this date the slash-command reference and the skills reference are one document.

The five-minute check you can run yourself

You need a directory that is not one of your real projects, so that no CLAUDE.md or existing skills leak into the runs. Create one and drop a single skill in it:

D=$(mktemp -d)
mkdir -p "$D/.claude/skills/echo-args"
cat > "$D/.claude/skills/echo-args/SKILL.md" <<'EOF'
---
description: Echo the arguments it received, for a measurement.
---
Reply with exactly this line and nothing else: ARGS=[$ARGUMENTS] A0=[$0] A1=[$1] A2=[$2] AB1=[$ARGUMENTS[1]]
EOF
cd "$D"
claude -p '/echo-args "quoted words" x' --output-format json --permission-mode default
Enter fullscreen mode Exit fullscreen mode

The JSON that comes back has a result field. Mine contained this, verbatim:

ARGS=["quoted words" x] A0=[quoted words] A1=[x] A2=[$2] AB1=[x]
Enter fullscreen mode Exit fullscreen mode

Terminal output of run 2: the substituted skill body keeps the quotes in $ARGUMENTS, strips them in $0, and leaves $2 as literal text

That single line already answers three questions. $ARGUMENTS kept the double quotes because it is the argument string "as typed". $0 received quoted words with the quotes stripped, so quoting groups words into one positional argument. And $2, which had nothing to receive, stayed in the body as the literal text $2.

But a model reply is a model reply. The line I care about is the one Claude Code built and sent, not the one the model chose to repeat. Claude Code writes every session to ~/.claude/projects/<escaped-cwd>/<session-id>.jsonl, where the escaped cwd is your directory path with slashes replaced by hyphens (for /private/tmp/skillargs.AKNnw7 it was -private-tmp-skillargs-AKNnw7). The session_id is in the same JSON output. Inside the transcript, a skill invocation under -p appears as two user messages. The first is the invocation itself:

<command-message>echo-args</command-message>
<command-name>/echo-args</command-name>
<command-args>"quoted words" x</command-args>
Enter fullscreen mode Exit fullscreen mode

The second is the substituted skill content, prefixed with one line Claude Code adds on its own:

Base directory for this skill: /private/tmp/skillargs.AKNnw7/.claude/skills/echo-args

Reply with exactly this line and nothing else: ARGS=["quoted words" x] A0=[quoted words] A1=[x] A2=[$2] AB1=[x]
Enter fullscreen mode Exit fullscreen mode

That second message is the ground truth for every claim in this article. For all 12 runs I compared it with the model's reply, and in 11 of them the reply matched the substituted line character for character. The exception (run 9) is discussed below, and it is a good reason to read transcripts rather than replies.

What the documentation says the rules are

Before the numbers, the documented contract, quoted from the skills page as fetched on 2026-09-22:

  • "Both you and Claude can pass arguments when invoking a skill. Arguments are available via the $ARGUMENTS placeholder."
  • "To access individual arguments by position, use $ARGUMENTS[N] or the shorter $N" — the example given is /migrate-component SearchBar JavaScript TypeScript, which "replaces $ARGUMENTS[0] with SearchBar, $ARGUMENTS[1] with JavaScript, and $ARGUMENTS[2] with TypeScript". Indexing is 0-based, so $0 is the first argument, not $1.
  • "Indexed arguments use shell-style quoting, so wrap multi-word values in quotes to pass them as a single argument. For example, /my-skill "hello world" second makes $0 expand to hello world and $1 to second. The $ARGUMENTS placeholder always expands to the full argument string as typed."
  • "An indexed placeholder with no corresponding argument, such as $2 when only one argument was passed, stays in the content unchanged. A named placeholder from the arguments frontmatter with no matching argument expands to an empty string."
  • "If you invoke a skill with arguments but no placeholder in the skill's content receives one, Claude Code appends ARGUMENTS: <your input> to the end of the skill content so Claude still sees what you typed."
  • "If you pass an argument value that itself contains text such as $1 or $ARGUMENTS, Claude Code inserts it as literal text and doesn't expand it."
  • The frontmatter table describes argument-hint as "Hint shown during autocomplete to indicate expected arguments. Example: [issue-number] or [filename] [format]." and arguments as "Named positional arguments for $name substitution in the skill content. Accepts a space-separated string or a YAML list. Names map to argument positions in order."

Documentation excerpt from the skills page: an indexed placeholder with no corresponding argument, such as $2 when only one argument was passed, stays in the content unchanged

Ten of my twelve runs confirmed these sentences exactly. Two runs found a case the page does not describe.

The four skills and the twelve runs

Besides echo-args above, I made three variants. echo-hint has the same body (minus the AB1 slot) plus argument-hint: [first] [second] [third] in its frontmatter. echo-named declares arguments: alpha beta and echoes ARGS=[$ARGUMENTS] ALPHA=[$alpha] BETA=[$beta] A0=[$0] A2=[$2]. echo-none has no placeholder at all; its body asks the model to reproduce the skill content it received, verbatim. All runs used claude -p "<prompt>" --output-format json --permission-mode default with the default model, no other flags. Here is what the substituted body contained in each run, copied from the transcripts.

Run 1, /echo-args foo bar baz: ARGS=[foo bar baz] A0=[foo] A1=[bar] A2=[baz] AB1=[bar]. The baseline. $ARGUMENTS[1] and $1 are the same slot.

Run 2, /echo-args "quoted words" x: ARGS=["quoted words" x] A0=[quoted words] A1=[x] A2=[$2] AB1=[x]. Shown above.

Run 3, /echo-args with no arguments: ARGS=[] A0=[$0] A1=[$1] A2=[$2] AB1=[$ARGUMENTS[1]]. This is the "arguments are missing" case. $ARGUMENTS became an empty string, while every indexed placeholder, including the long form $ARGUMENTS[1], stayed as literal text. If your skill body says "Fix issue $0", a bare invocation will hand the model the sentence "Fix issue $0", which reads like a template that was never filled in. The transcript's <command-args> element was simply absent for this run.

Run 4, /echo-args price-is-$1.50 $ARGUMENTS: ARGS=[price-is-$1.50 $ARGUMENTS] A0=[price-is-$1.50] A1=[$1] A2=[$2] AB1=[$ARGUMENTS[1]]. Two things happened. The $1 inside price-is-$1.50 was not expanded, in line with the "inserts it as literal text" sentence; $0 got the value intact. But the second argument, the bare token $ARGUMENTS, did not arrive in $1. The slot stayed as the literal $1, as if only one argument had been passed, while the full $ARGUMENTS string still shows both tokens.

Run 5, a 1,504-character first argument followed by second: I passed 1,500 L characters plus -END as the first argument. The transcript shows ARGS holding all 1,511 characters and A0 holding all 1,504, ending in LL-END, with A1=[second], A2=[$2], and AB1=[second]. Substitution had no problem with the length. The model did. It started writing ARGS=[LLLL... and never found the end: the first assistant message stopped with stop_reason: max_tokens after exactly 64,000 output tokens, 63,994 of them the letter L. Claude Code then injected a "Output token limit hit. Resume directly" message, and the model replied that it could not reproduce the line and summarised the slots in prose (correctly). The run took 527 seconds and the JSON reported total_cost_usd of 5.16, against 0.23 to 0.30 for every other run. The lesson is not about substitution; it is that "echo this back" is a dangerous instruction when the argument is long and repetitive, because a model cannot count.

Run 6, /echo-hint foo bar baz and Run 7, /echo-hint: ARGS=[foo bar baz] A0=[foo] A1=[bar] A2=[baz] and ARGS=[] A0=[$0] A1=[$1] A2=[$2]. Identical to runs 1 and 3. The argument-hint line did not appear anywhere in either transcript, did not fill missing slots, and did not add a note to the body. Under -p there is no autocomplete, so the field is inert there, which is consistent with the documented description of it as an autocomplete hint. It is still worth writing for humans in interactive sessions; it just does not reach the model.

Run 8, /echo-named foo: ARGS=[foo] ALPHA=[foo] BETA=[] A0=[foo] A2=[$2]. The named argument $beta, with no value at position 1, expanded to an empty string, while the indexed $2 in the same body stayed literal. Both behaviours match the documentation sentence in the image above, and this run shows them side by side in one line: named placeholders disappear cleanly, indexed ones leave a trace.

Run 9, /echo-none foo bar: the transcript body was the skill's own sentence, then two empty lines, then ARGUMENTS: foo bar. So the documented fallback works: with no placeholder, Claude Code appends ARGUMENTS: <your input> at the end. Now the caveat I promised. The model's reply, which I had asked to be the complete skill content reproduced verbatim, was only the original sentence. It dropped the ARGUMENTS: foo bar line entirely. If I had trusted the reply, I would have concluded that the fallback does not fire. This is the run that convinced me to treat transcripts as the only evidence.

Run 10, /echo-args a b c d: ARGS=[a b c d] A0=[a] A1=[b] A2=[c] AB1=[b]. A fourth argument with no slot is not an error and triggers no ARGUMENTS: line, because other placeholders received values. It simply lives only inside $ARGUMENTS.

Run 11, /echo-args 'single quoted' "double quoted" plain: ARGS=['single quoted' "double quoted" plain] A0=[single quoted] A1=[double quoted] A2=[plain] AB1=[double quoted]. Single and double quotes both group words and both are stripped from the positional slots, while $ARGUMENTS keeps them. Note that I passed the prompt to claude -p inside a shell string, so these quotes reached Claude Code intact; the <command-args> element in the transcript confirms it.

Run 12, /echo-args "$ARGUMENTS from yesterday" $HOME: ARGS=["$ARGUMENTS from yesterday" $HOME] A0=[$ARGUMENTS from yesterday] A1=[$1] A2=[$2] AB1=[$ARGUMENTS[1]]. This is the documented example almost word for word, and the quoted half behaved as documented: $0 received the text $ARGUMENTS from yesterday and nothing inside it was expanded. The second token, $HOME, was passed in single-quoted shell syntax so my shell did not touch it, and the transcript's <command-args> shows it arriving as $HOME. Yet $1 stayed literal. The bare token disappeared from the positional list, just like bare $ARGUMENTS in run 4.

The one behaviour the page does not describe

Two runs (4 and 12), two different bare tokens ($ARGUMENTS and $HOME), the same result: the token is present in $ARGUMENTS, absent from the positional slots, and the slot count is one short. The documentation covers quoted values containing $1 or $ARGUMENTS (run 12 confirms that case) and covers escaping with a backslash, but it does not say what happens to an unquoted token that consists of a dollar sign followed by a name.

My working hypothesis is that the "shell-style quoting" used to split positional arguments also performs shell-style variable expansion with no variables defined, so $HOME and $ARGUMENTS become empty and the empty token is dropped, while $1.50 survives because a digit is not a valid variable name. I did not verify this hypothesis; I stopped at 12 runs and did not read the source. Treat it as a guess and the observed disappearance as the fact. The practical advice does not depend on the mechanism: if an argument may contain a dollar sign followed by letters, quote the whole value. Quoted values arrived intact in every run where I used them.

What this means for writing skill bodies

Putting the runs together, the contract I now design against on 2.1.278 looks like this. Use $ARGUMENTS when the skill should see everything the user typed, quotes and all; it is never literal and is empty rather than absent when nothing was passed. Use $0, $1, $2 (or $ARGUMENTS[N]) when the arguments have fixed roles, and remember that an unfilled slot leaves its own name in the prose. If a slot may legitimately be omitted, declare it under arguments: and refer to it by name; run 8 shows the named form vanishing cleanly where the indexed form would have left $2 behind. If a skill has no placeholders, you still get the input, appended as ARGUMENTS: ... at the very end of the body, which is fine for short notes and easy to overlook for anything structured.

argument-hint is documentation for the person at the keyboard. In seven runs it never changed a byte of what the model saw, so do not rely on it to communicate expectations to the model; put those expectations in the body.

And when you are measuring any of this, do not ask the model to tell you what it received. Run 9 showed the model quietly omitting a line while claiming a verbatim reproduction, and run 5 showed it burning 64,000 output tokens on a string it could not count. The transcript under ~/.claude/projects/ costs nothing to read and is the only place where the substituted text exists unedited.

Numbers, for the record

Twelve claude -p runs in total, all on Claude Code 2.1.278, default model, default permission mode, in a fresh mktemp -d directory with four skills and no CLAUDE.md. Eleven runs finished in 4.4 to 16.8 seconds with 30 to 79 output tokens each and a reported cost between 0.23 and 0.30 USD; the long-argument run took 527 seconds, 64,812 output tokens and 5.16 USD. Substituted bodies matched model replies in 11 of 12 runs; the mismatch was the omitted ARGUMENTS: line in run 9. Documented behaviours confirmed: $ARGUMENTS as typed, 0-based $N and $ARGUMENTS[N], shell-style quoting, literal leftover for missing indexed slots, empty string for missing named slots, ARGUMENTS: fallback, and literal insertion of quoted values containing $ARGUMENTS. Behaviour not described in the page: bare $NAME tokens dropping out of the positional slots (2 of 2 attempts). If you rerun the check above on a newer version and the $HOME case behaves differently, I would like to know; the four SKILL.md files fit in a single shell heredoc and the whole measurement, minus the long-argument mistake, costs about three dollars and two minutes.


Rulestack writes skills, slash commands and rules files for Claude Code and sells them at rulestack.gumroad.com. The four echo skills from this article fit in one heredoc, which makes them a cheap check to rerun before shipping a skill that takes arguments.

If your own runs show a different split for quoted or $-prefixed arguments, reply on @ai-shop.bsky.social; we will rerun the twelve with your input.

Top comments (0)