Skip to content

Bug: slash-invoked skills replace user prompt with full SKILL.md (arguments swallowed / lost at end) #40463

Description

@BrickerP

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:

  1. Swallowed if the skill body happens to contain $ARGUMENTS / $1 / $2 (common in skill docs that show examples), or
  2. 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

  1. 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.
  1. In a new session, run:
/demo-skill please fix the login bug in src/auth.ts
  1. 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.
  1. 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:

  1. Load/inject the skill instructions, and
  2. 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

  1. Do not treat raw skill body as a command template subject to $ARGUMENTS/$N replacement.
  2. 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()
  1. Only substitute $ARGUMENTS (and optional $NAME / $SKILL_DIR) on the wrapper, never on $SKILL_BODY.
  2. Optionally strip or escape $ARGUMENTS/$N inside skill bodies when registering, so docs cannot collide.
  3. 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:

  1. Prefer natural language: use demo-skill: please fix ... so the agent calls the skill tool.
  2. 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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions