Summary
When invoking a skill via slash command (/skill-name my actual request), OpenCode replaces the entire user message with the skill's full SKILL.md body. The user's trailing arguments are either:
- Swallowed if the skill body happens to contain
$ARGUMENTS / $1 / $2 (common in skill docs that show examples), or
- Appended at the very end of a long skill document when no placeholders exist — so the model effectively only "sees" the skill manual and responds with something like "what do you mean?" / asks for clarification.
This makes slash-invoked skills unusable for any non-trivial skill that includes documentation examples or is longer than a short checklist.
Environment
- OpenCode version:
1.18.13 (opencode-ai / opencode-darwin-arm64)
- Invocation: TUI slash command
/<skill-name> <user prompt>
- Skills loaded from:
~/.config/opencode/skills, ~/.claude/skills, project .opencode/skills, etc.
Reproduction
- Create a skill with a non-trivial body, e.g.
.opencode/skills/demo-skill/SKILL.md:
---
name: demo-skill
description: Demo skill that reproduces argument swallowing
---
# Demo Skill
Do the following steps carefully.
### Example (from docs)
When the user says something like:
Input: $ARGUMENTS
you should parse it.
Also support `$1` positional args in examples.
- In a new session, run:
/demo-skill please fix the login bug in src/auth.ts
- Observe the user message that actually reaches the model:
- The message is the entire skill template, not the user's request.
- Because the body contains
$ARGUMENTS / $1, the substitution logic fills those placeholders inside the documentation examples, not as a dedicated "user request" section.
- The model often replies as if no concrete task was given.
- Variant without placeholders: remove
$ARGUMENTS / $1 from the skill body and re-run. User text is appended after the full skill markdown. With long skills, the real request is buried at the bottom and frequently ignored.
Expected behavior
Slash-invoking a skill should:
- Load/inject the skill instructions, and
- Preserve the user's request as a first-class, clearly labeled section that is not subject to accidental placeholder collisions inside the skill body.
Something like:
Follow the skill instructions below for skill "demo-skill".
<skill>
...skill body, NOT scanned for $ARGUMENTS/$N...
</skill>
## User request
please fix the login bug in src/auth.ts
Alternatively: keep slash skills as thin wrappers that always use an outer template with $ARGUMENTS, and never run $ARGUMENTS/$N replacement against the raw skill body.
Actual behavior (from 1.18.13 binary)
Skills are registered as slash commands with source: "skill" and template = full skill content (+ base directory footer):
// Command registration for each skill
s[o.name] = {
name: o.name,
description: o.description,
source: "skill",
get template() {
if (!t) return o.content
return [
o.content,
"",
`Base directory for this skill: ${t}`,
"Relative paths in this skill (e.g., scripts/, references/) are relative to this base directory.",
].join("\n")
},
}
Command execution then does placeholder substitution on that template:
// SessionPrompt.command (simplified from 1.18.13)
let args = (t.arguments.match(tokenRe) ?? []).map(...)
let template = await V.template
// replace $1, $2, ...
template = template.replaceAll(/\$(\d+)/g, ...)
// replace $ARGUMENTS
let hasArguments = template.includes("$ARGUMENTS")
template = template.replaceAll("$ARGUMENTS", t.arguments)
// only if NEITHER $N nor $ARGUMENTS existed, append user args
if (no $N placeholders && !hasArguments && t.arguments.trim()) {
template = template + "\n" + t.arguments
}
So:
| Skill body |
What happens to user args |
Contains $ARGUMENTS (even in examples) |
Injected into those example spots; no dedicated user-request section |
Contains $1/$2 only |
Same — fills doc examples |
| Contains neither |
Appended after entire skill MD (easy to miss) |
Contrast with the skill tool path (skill({ name })), which correctly returns <skill_content> as a tool result and leaves the original user prompt intact. The bug is specifically in slash → command → template substitution, not in skill discovery/loading itself.
Impact
- Any skill whose docs mention
$ARGUMENTS / $1 (or copy Claude Code / command examples) silently corrupts the user request.
- Long skills (common for project workflows) push the real task past the model's attention window for the user turn.
- Users learn to avoid
/skill entirely and instead write "use X skill: …" so the agent loads via the skill tool — defeating the purpose of slash invocation.
Suggested fix
- Do not treat raw skill body as a command template subject to
$ARGUMENTS/$N replacement.
- Wrap skill slash invocation in a fixed outer template, e.g.:
const SKILL_SLASH_TEMPLATE = `
Follow skill "$NAME".
<skill_content name="$NAME">
$SKILL_BODY
</skill_content>
Base directory: $SKILL_DIR
## User request
$ARGUMENTS
`.trim()
- Only substitute
$ARGUMENTS (and optional $NAME / $SKILL_DIR) on the wrapper, never on $SKILL_BODY.
- Optionally strip or escape
$ARGUMENTS/$N inside skill bodies when registering, so docs cannot collide.
- Add a regression test:
- skill body contains the literal string
$ARGUMENTS in an example
- invoke
/skill-name do the thing
- assert the model-facing user text contains a clear
User request section with do the thing, and the example $ARGUMENTS remains literal (or is escaped), not replaced.
Workaround (current)
Until fixed:
- Prefer natural language:
use demo-skill: please fix ... so the agent calls the skill tool.
- Or put
$ARGUMENTS at the top of every SKILL.md under a ## User request heading, and remove other $ARGUMENTS/$N occurrences from the body (or write them as `$ARGUMENTS` only if the engine still matches — currently it matches raw $ARGUMENTS anywhere).
Related
- Skills docs: https://opencode.ai/docs/skills/
- Commands use
$ARGUMENTS intentionally; skills should not inherit that template semantics when the body is free-form markdown documentation.
Happy to test a PR against 1.18.x / dev.
Summary
When invoking a skill via slash command (
/skill-name my actual request), OpenCode replaces the entire user message with the skill's fullSKILL.mdbody. The user's trailing arguments are either:$ARGUMENTS/$1/$2(common in skill docs that show examples), orThis makes slash-invoked skills unusable for any non-trivial skill that includes documentation examples or is longer than a short checklist.
Environment
1.18.13(opencode-ai/opencode-darwin-arm64)/<skill-name> <user prompt>~/.config/opencode/skills,~/.claude/skills, project.opencode/skills, etc.Reproduction
.opencode/skills/demo-skill/SKILL.md:$ARGUMENTS/$1, the substitution logic fills those placeholders inside the documentation examples, not as a dedicated "user request" section.$ARGUMENTS/$1from the skill body and re-run. User text is appended after the full skill markdown. With long skills, the real request is buried at the bottom and frequently ignored.Expected behavior
Slash-invoking a skill should:
Something like:
Alternatively: keep slash skills as thin wrappers that always use an outer template with
$ARGUMENTS, and never run$ARGUMENTS/$Nreplacement against the raw skill body.Actual behavior (from 1.18.13 binary)
Skills are registered as slash commands with
source: "skill"andtemplate= full skill content (+ base directory footer):Command execution then does placeholder substitution on that template:
So:
$ARGUMENTS(even in examples)$1/$2onlyContrast with the skill tool path (
skill({ name })), which correctly returns<skill_content>as a tool result and leaves the original user prompt intact. The bug is specifically in slash → command → template substitution, not in skill discovery/loading itself.Impact
$ARGUMENTS/$1(or copy Claude Code / command examples) silently corrupts the user request./skillentirely and instead write "use X skill: …" so the agent loads via the skill tool — defeating the purpose of slash invocation.Suggested fix
$ARGUMENTS/$Nreplacement.$ARGUMENTS(and optional$NAME/$SKILL_DIR) on the wrapper, never on$SKILL_BODY.$ARGUMENTS/$Ninside skill bodies when registering, so docs cannot collide.$ARGUMENTSin an example/skill-name do the thingUser requestsection withdo the thing, and the example$ARGUMENTSremains literal (or is escaped), not replaced.Workaround (current)
Until fixed:
use demo-skill: please fix ...so the agent calls theskilltool.$ARGUMENTSat the top of every SKILL.md under a## User requestheading, and remove other$ARGUMENTS/$Noccurrences from the body (or write them as`$ARGUMENTS`only if the engine still matches — currently it matches raw$ARGUMENTSanywhere).Related
$ARGUMENTSintentionally; skills should not inherit that template semantics when the body is free-form markdown documentation.Happy to test a PR against 1.18.x / dev.