Skip to content

Latest commit

 

History

History
199 lines (156 loc) · 6.52 KB

File metadata and controls

199 lines (156 loc) · 6.52 KB

14.3 Command System

Model: claude-opus-4-6 (anthropic/claude-opus-4-6) Generation Date: 2026-02-17


The Command system is another extension point for user interaction in OpenCode -- it allows users to define custom Slash Commands, turning frequently used prompts into templates with parameters for one-click triggering of complex operations.

14.3.1 Custom Slash Command Mechanism

Slash Commands start with /. Users type commands like /init or /review in the TUI input box to trigger predefined operations.

The Command data model:

// command/index.ts
export const Info = z.object({
  name: z.string(),                    // Command name
  description: z.string().optional(),  // Description
  agent: z.string().optional(),        // Designated Agent
  model: z.string().optional(),        // Designated model
  source: z.enum(["command", "mcp", "skill"]).optional(),  // Source
  template: z.promise(z.string()).or(z.string()),           // Prompt template
  subtask: z.boolean().optional(),     // Whether to execute as a subtask
  hints: z.array(z.string()),          // Parameter hint list
})

Template Variables

Command templates support two types of variables:

  • Numbered variables ($1, $2, $3...): Replaced positionally with user-provided arguments.
  • $ARGUMENTS: Replaced with the user's complete argument string.
# Template example for the /review command
Review the changes in ${path}.
$1 defaults to uncommitted changes.
Focus on: $ARGUMENTS

The hints() function extracts all variables from the template, used to display parameter hints in the TUI:

export function hints(template: string): string[] {
  const result: string[] = []
  const numbered = template.match(/\$\d+/g)
  if (numbered) {
    for (const match of [...new Set(numbered)].sort()) result.push(match)
  }
  if (template.includes("$ARGUMENTS")) result.push("$ARGUMENTS")
  return result
}

14.3.2 Built-in Commands

OpenCode includes two built-in commands:

const Default = {
  INIT: "init",
  REVIEW: "review",
} as const

const result: Record<string, Info> = {
  [Default.INIT]: {
    name: "init",
    description: "create/update AGENTS.md",
    source: "command",
    get template() {
      return PROMPT_INITIALIZE.replace("${path}", Instance.worktree)
    },
    hints: hints(PROMPT_INITIALIZE),
  },
  [Default.REVIEW]: {
    name: "review",
    description: "review changes [commit|branch|pr], defaults to uncommitted",
    source: "command",
    get template() {
      return PROMPT_REVIEW.replace("${path}", Instance.worktree)
    },
    subtask: true,
    hints: hints(PROMPT_REVIEW),
  },
}
  • /init: Guides the Agent to create or update the project's AGENTS.md file (project specification document).
  • /review: Has the Agent review code changes. subtask: true means this command creates a sub-session for execution, without affecting the current session's context.

14.3.3 Three Command Sources

The Command system unifies commands from three different sources:

User-Configured Commands

Defined through the command field in opencode.json:

{
  "command": {
    "test": {
      "description": "Run and fix failing tests",
      "template": "Run `npm test` in ${path}. If any tests fail, fix them. $ARGUMENTS",
      "agent": "build"
    }
  }
}

MCP Prompt Commands

Prompts exposed by MCP Servers are automatically registered as commands:

for (const [name, prompt] of Object.entries(await MCP.prompts())) {
  result[name] = {
    name,
    source: "mcp",
    description: prompt.description,
    get template() {
      // Asynchronously fetch MCP Prompt content
      return new Promise<string>(async (resolve, reject) => {
        const template = await MCP.getPrompt(prompt.client, prompt.name, /* args */)
        resolve(template?.messages.map(m => m.content.text).join("\n") || "")
      })
    },
    hints: prompt.arguments?.map((_, i) => `$${i + 1}`) ?? [],
  }
}

Note that template uses a getter + Promise -- because MCP Prompt content needs to be fetched asynchronously from the MCP Server. The Zod Schema uses z.promise(z.string()).or(z.string()) to support both synchronous and asynchronous templates.

Skill Commands

Each Skill is also registered as a command (unless a command with the same name already exists):

for (const skill of await Skill.all()) {
  if (result[skill.name]) continue  // Don't override existing commands
  result[skill.name] = {
    name: skill.name,
    description: skill.description,
    source: "skill",
    get template() {
      return skill.content
    },
    hints: [],
  }
}

When a user types /git-master, the complete content of the git-master Skill is sent to the Agent as a prompt. This provides a shortcut -- instead of the Agent deciding which Skill to load on its own, the user directly specifies it via a command.

14.3.4 Command Priority

When commands with the same name appear from the three sources, the priority is:

Built-in commands > User-configured commands > MCP Prompt commands > Skill commands

This priority ensures that built-in functionality cannot be accidentally overridden, while user configuration can override commands from external sources.

14.3.5 Command Events

When a command is executed, an event is published:

export const Event = {
  Executed: BusEvent.define("command.executed", z.object({
    name: z.string(),
    sessionID: Identifier.schema("session"),
    arguments: z.string(),
    messageID: Identifier.schema("message"),
  })),
}

This event is captured by the Plugin's command.execute.before hook, allowing Plugins to inject additional context before a command is executed.

14.3.6 Summary

The Command system unifies commands from three different sources (user configuration, MCP Prompts, Skills) under a single interface, providing a consistent user experience:

Feature User-Configured MCP Prompt Skill
Definition location opencode.json MCP Server SKILL.md
Template Synchronous string Asynchronous Promise Synchronous string
Parameters $1, $ARGUMENTS $1, $2 No parameters
Can specify Agent Yes No No
Can specify Model Yes No No
Subtask mode Yes No No

This unified design means users don't need to worry about where a command comes from -- whether it's a hand-written template, a Prompt provided by an MCP Server, or a Skill file, they can all be triggered with a single /command-name input.