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 , andargument-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" / .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: echo-args /echo-args "quoted words" x 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] 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] withSearchBar ,$ARGUMENTS[1] withJavaScript , and$ARGUMENTS[2] withTypeScript ". 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 tohello world and$1 tosecond . 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 thearguments 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: 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] ." andarguments 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." 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 " " --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 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: 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 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 t
Comments
No comments yet. Start the discussion.