An interpretation of the Anthropic Agent Skills Specification for OpenCode, providing lazy-loaded skill discovery and injection.
Differentiators include:
skill_find words, words words to discover skillsskill_use "skill_name" and,skill_resource skill_relative/resource/path to read reference materialGet up and running with three common tasks:
skill_find "git commit"
Searches for skills related to writing git commits. Returns matching skills sorted by relevance.
skill_use "experts_writing_git_commits"
Loads the skill into your chat context. The AI agent can now reference it when giving advice.
skill_resource skill_name="experts_writing_git_commits" relative_path="references/guide.md"
Access specific documentation or templates from a skill without loading the entire skill.
Next steps: See Usage Examples for real-world scenarios, or jump to Plugin Tools for detailed documentation.
Create or edit your OpenCode configuration file (typically ~/.config/opencode/config.json):
{
"plugins": ["@zenobius/opencode-skillful"]
}
This plugin takes a different approach than OpenCode's built-in skills implementation:
| Aspect | Built-in OpenCode | opencode-skillful |
|---|---|---|
| Skill Loading | All skills pre-loaded into context by default | Skills loaded on-demand only |
| Memory Overhead | All skills consume tokens in every conversation | Only loaded skills consume tokens |
| Format Configuration | Fixed format (usually markdown) | Per-model configuration (JSON, XML, Markdown) |
| Skill Discovery | Limited, built-in set | Extensible, custom skills in any directory |
| Resource Access | Direct filesystem (less secure) | Pre-indexed resources (security-first) |
Unlike built-in skills that load automatically, opencode-skillful uses lazy loading:
Different LLM providers prefer different formats. Configure which format each model receives:
{
"promptRenderer": "xml",
"modelRenderers": {
"claude-3-5-sonnet": "xml",
"gpt-4": "json",
"gpt-4-turbo": "json",
"llama-2-70b": "md"
}
}
This allows you to optimize skill injection for each model without creating duplicate skill documentation.
Skills can include executable scripts in their scripts/ directory. When a skill is loaded with skill_use, the agent receives the full inventory of available scripts and can be instructed to run them.
How it works:
./scripts/setup.shskill_use, the agent sees all available scripts in the resource inventoryExample:
Your skill's SKILL.md includes: ./scripts/generate-changelog.sh
You ask: "Can you run the changelog generator from the build-utils skill?"
The agent:
skill_usescripts/generate-changelog.shWhy this approach?
Rather than a dedicated script execution tool, agents have full visibility into all skill resources and can intelligently decide when and how to run them based on your instructions and the task context. Scripts are referenced naturally in skill documentation (e.g., "Run ./scripts/setup.sh to initialize") and agents can work out the paths and execution from context.
The plugin provides three simple but powerful tools:
| Tool | Purpose | When to Use |
|---|---|---|
| skill_find | Discover skills by keyword | You want to search for relevant skills |
| skill_use | Load skills into chat | You want the AI to reference a skill |
| skill_resource | Read specific files from skills | You need a template, guide, or script from a skill |
See Plugin Tools for complete documentation.
These real-world scenarios show how to use the three tools together:
You want: The AI to help you write a commit message following best practices.
Steps:
Search for relevant skills:
skill_find "git commit"
Load the skill into your chat:
skill_use "experts_writing_git_commits"
Ask the AI: "Help me write a commit message for refactoring the auth module"
The AI now has the skill's guidance and can apply best practices to your request.
You want: Access a specific template or guide from a skill without loading the entire skill.
Steps:
Find skills in a category:
skill_find "testing"
Once you've identified a skill, read its resources:
skill_resource skill_name="testing-skill" relative_path="references/test-template.md"
This is useful when you just need a template or specific document, not full AI guidance.
You want: See what skills are available under a specific category.
Steps:
List all skills:
skill_find "*"
Filter by category:
skill_find "experts"
Search with exclusions:
skill_find "testing -performance"
This searches for testing-related skills but excludes performance testing.
* or empty: List all skillskeyword1 keyword2: AND logic (all terms must match)-term: Exclude results matching this term"exact phrase": Match exact phraseexperts, superpowers/writing: Path prefix matchingThe plugin supports multiple formats for prompt injection, allowing you to optimize results for different LLM models and use cases.
See the Configuration section for complete configuration details, including bunfig setup and global/project-level overrides.
Choose the format that works best for your LLM:
| Format | Best For | Characteristics |
|---|---|---|
| XML (default) | Claude models | Human-readable, structured, XML-optimized for Claude |
| JSON | GPT and strict parsers | Machine-readable, strict JSON structure, strong parsing support |
| Markdown | All models | Readable prose, heading-based structure, easy to read in conversations |
Set your preferences in .opencode-skillful.json:
{
"promptRenderer": "xml",
"modelRenderers": {
"claude-3-5-sonnet": "xml",
"gpt-4": "json",
"gpt-4-turbo": "json"
}
}
How It Works:
modelRenderers[modelID] is configured, that format is usedpromptRenderer default is used<Skill>
<name>git-commits</name>
<description>Guidelines for writing effective git commit messages</description>
<toolName>writing_git_commits</toolName>
</Skill>
Advantages:
{
"name": "git-commits",
"description": "Guidelines for writing effective git commit messages",
"toolName": "writing_git_commits"
}
Advantages:
# Skill
### name
- **name**: _git-commits_
### description
- **description**: _Guidelines for writing effective git commit messages_
### toolName
- **toolName**: _writing_git_commits_
Advantages:
Skills are discovered from these locations (in priority order, last wins on duplicates):
~/.config/opencode/skills/ - Standard XDG config location or (%APPDATA%/.opencode/skills for windows users).opencode/skills/ - Project-local skills (highest priority)# List all available skills
skill_find query="*"
# Search by keyword
skill_find query="git commit"
# Exclude terms
skill_find query="testing -performance"
Skills must be loaded by their fully-qualified identifier (generated from the directory path).
The skill identifier is created by converting the directory path to a slug: directory separators and hyphens become underscores.
skills/
experts/
ai/
agentic-engineer/ # Identifier: experts_ai_agentic_engineer
superpowers/
writing/
code-review/ # Identifier: superpowers_writing_code_review
When loading skills, use the full identifier:
# Load a skill by its identifier
skill_use "experts_ai_agentic_engineer"
# Load multiple skills
skill_use "experts_ai_agentic_engineer", "superpowers_writing_code_review"
# Read a reference document
skill_resource skill_name="writing-git-commits" relative_path="references/style-guide.md"
# Read a template
skill_resource skill_name="brand-guidelines" relative_path="assets/logo-usage.html"
Normally you won't need to use skill_resource directly, since the llm will do it for you.
When skill_use is called, it will be wrapped with instructions to the llm on how to load references, assets, and scripts from the skill.
But when writing your skills, there's nothing stopping you from using skill_resource to be explicit.
(Just be aware that exotic file types might need special tooling to handle them properly.)
The plugin provides three core tools implemented in src/tools/:
skill_find (SkillFinder.ts)Search for skills using natural query syntax with intelligent ranking by relevance.
Parameters:
query: Search string supporting:
* or empty: List all skillsexperts, superpowers/writing (prefix matching)git commit (multiple terms use AND logic)-term (exclude results)"exact match" (phrase matching)Returns:
skill_name: Skill identifier (e.g., experts_writing_git_commits)skill_shortname: Directory name of the skill (e.g., writing-git-commits)description: Human-readable descriptionExample Response:
<SkillSearchResults query="git commit">
<Skills>
<Skill skill_name="experts_writing_git_commits" skill_shortname="writing-git-commits">
Guidelines for writing effective git commit messages
</Skill>
</Skills>
<Summary>
<Total>42</Total>
<Matches>1</Matches>
<Feedback>Found 1 skill matching your query</Feedback>
</Summary>
</SkillSearchResults>
Then load it with:
skill_use "experts_writing_git_commits"
skill_use (SkillUser.ts)Load one or more skills into the chat context with full resource metadata.
Parameters:
skill_names: Array of skill identifiers to load (e.g., experts_ai_agentic_engineer)Features:
references: Documentation and reference filesassets: Binary files, templates, imagesscripts: Executable scripts with paths and MIME typesBehavior: Silently injects skills as user messages (persists in conversation history)
Example Response:
{ "loaded": ["experts/writing-git-commits"], "notFound": [] }
skill_resource (SkillResourceReader.ts)Read a specific resource file from a skill's directory and inject silently into the chat.
When to Use:
skill_use to access its resourcesParameters:
skill_name: The skill containing the resource (by toolName/FQDN or short name)relative_path: Path to the resource relative to the skill directoryResource Types:
Can read any of the three resource types:
references/ - Documentation, guides, and reference materialsassets/ - Templates, images, binary files, and configuration filesscripts/ - Executable scripts (shell, Python, etc.) for viewing or analysisReturns:
Behavior:
references/guide.md, assets/template.html, scripts/setup.sh)Example Responses:
Reference document:
Load Skill Resource
skill: experts/writing-git-commits
resource: references/commit-guide.md
type: text/markdown
Script file:
Load Skill Resource
skill: build-utils
resource: scripts/generate-changelog.sh
type: text/plain
Asset file:
Load Skill Resource
skill: brand-guidelines
resource: assets/logo-usage.html
type: text/html
When a search query doesn't match any skills, the response includes feedback:
<SkillSearchResults query="nonexistent">
<Skills></Skills>
<Summary>
<Total>42</Total>
<Matches>0</Matches>
<Feedback>No skills found matching your query</Feedback>
</Summary>
</SkillSearchResults>
When loading skills, if a skill name is not found, it's returned in the notFound array:
{ "loaded": ["writing-git-commits"], "notFound": ["nonexistent-skill"] }
The loaded skills are still injected into the conversation, allowing you to use available skills even if some are not found.
Resource loading failures occur when:
The tool returns an error message indicating which skill or resource path was problematic.
The plugin loads configuration from bunfig, supporting both project-local and global configuration files:
Configuration is loaded in this priority order (highest priority last):
Global config (standard platform locations):
~/.config/opencode-skillful/config.json%APPDATA%/opencode-skillful/config.jsonProject config (in your project root):
.opencode-skillful.jsonLater configuration files override earlier ones. Use project-local .opencode-skillful.json to override global settings for specific projects.
First, register the plugin in your OpenCode config (~/.config/opencode/config.json):
{
"plugins": ["@zenobius/opencode-skillful"]
}
Create .opencode-skillful.json in your project root or global config directory:
{
"debug": false,
"basePaths": ["~/.config/opencode/skills", ".opencode/skills"],
"promptRenderer": "xml",
"modelRenderers": {}
}
Configuration Fields:
debug (boolean, default: false): Enable debug output showing skill discovery stats
skill_find responses include discovered, parsed, rejected, and error countsbasePaths (array, default: standard locations): Custom skill search directories
[~/.config/opencode/skills, .opencode/skills].opencode/skills/ for project-specific skillspromptRenderer (string, default: 'xml'): Default format for prompt injection
'xml' | 'json' | 'md'modelRenderers (object, default: {}): Per-model format overrides
promptRenderer for specific models{ "gpt-4": "json", "claude-3-5-sonnet": "xml" }When any tool executes (skill_find, skill_use, skill_resource):
"anthropic-claude-3-5-sonnet")"claude-3-5-sonnet")modelRenderers configuration
modelRenderers, falls back to promptRenderer defaultExample: If your config has "claude-3-5-sonnet": "xml" and the active model is "anthropic-claude-3-5-sonnet", the plugin will:
"anthropic-claude-3-5-sonnet" (no match)"claude-3-5-sonnet" (match found! Use XML)"xml" formatThis allows different models to receive results in their preferred format without needing to specify every model variant. Configure the generic model name once and it works for all provider-prefixed variations.
~/.config/opencode-skillful/config.json:
{
"debug": false,
"promptRenderer": "xml",
"modelRenderers": {
"claude-3-5-sonnet": "xml",
"claude-3-opus": "xml",
"gpt-4": "json",
"gpt-4-turbo": "json",
"llama-2-70b": "md"
}
}
.opencode-skillful.json (project root):
{
"debug": true,
"basePaths": ["~/.config/opencode/skills", ".opencode/skills", "./vendor/skills"],
"promptRenderer": "xml",
"modelRenderers": {
"gpt-4": "json"
}
}
This project-local config:
The plugin uses a layered, modular architecture with clear separation of concerns and two-phase initialization.
┌──────────────────────────────────────────────────────┐
│ Plugin Entry Point (index.ts) │
│ - Defines 3 core tools: skill_find, skill_use, │
│ skill_resource │
│ - NOT including skill_exec (removed) │
│ - Initializes API factory and config │
│ - Manages message injection (XML serialization) │
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ API Factory Layer (api.ts, config.ts) │
│ - createApi(): initializes logger, registry, tools │
│ - getPluginConfig(): resolves base paths with proper │
│ precedence (project-local overrides global) │
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ Services Layer (src/services/) │
│ - SkillRegistry: discovery, parsing, resource │
│ mapping with ReadyStateMachine coordination │
│ - SkillSearcher: query parsing + intelligent ranking│
│ - SkillResourceResolver: safe path-based retrieval │
│ - SkillFs (via lib): filesystem abstraction (mockable)
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ Core Libraries (src/lib/) │
│ - ReadyStateMachine: async initialization sequencing │
│ - SkillFs: abstract filesystem operations │
│ - Identifiers: path ↔ tool name conversions │
│ - OpenCodeChat: message injection abstraction │
│ - xml.ts: JSON → XML serialization │
└──────────────────────────────────────────────────────┘
The plugin uses a two-phase initialization pattern to coordinate async discovery with tool execution:
PHASE 1: SYNCHRONOUS CREATION
├─ Plugin loads configuration
├─ createApi() called:
│ ├─ Creates logger
│ ├─ Creates SkillRegistry (factory, NOT initialized yet)
│ └─ Returns registry + tool creators
└─ index.ts registers tools immediately
PHASE 2: ASYNCHRONOUS DISCOVERY (Background)
├─ registry.initialise() called
│ ├─ Sets ready state to "loading"
│ ├─ Scans all base paths for SKILL.md files
│ ├─ Parses each file (YAML frontmatter + resources)
│ ├─ Pre-indexes all resources (scripts/assets/references)
│ └─ Sets ready state to "ready"
│
└─ WHY PRE-INDEXING: Prevents path traversal attacks.
Tools can only retrieve pre-registered resources,
never arbitrary filesystem paths.
PHASE 3: TOOL EXECUTION (User Requested)
├─ User calls: skill_find("git commits")
├─ Tool executes:
│ ├─ await registry.controller.ready.whenReady()
│ │ (blocks until Phase 2 completes)
│ ├─ Search registry
│ └─ Return results
└─ WHY THIS PATTERN: Multiple tools can call whenReady()
at different times without race conditions.
Simple Promise would resolve once; this allows
concurrent waiters.
┌─────────────────────────────────────────┐
│ User Query: skill_find("git commit") │
└────────────┬────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ SkillFinder Tool (tools/SkillFinder.ts) │
│ - Validates query string │
│ - Awaits: controller.ready.whenReady() │
└────────────┬────────────────────────────┘
↓ (continues when registry ready)
┌─────────────────────────────────────────┐
│ SkillSearcher.search() │
│ - Parse query via search-string library │
│ ("git commit" → include: [git,commit])│
│ - Filter ALL include terms must match │
│ (name, description, or toolName) │
│ - Exclude results matching exclude │
│ terms │
│ - Rank results: │
│ * nameMatches × 3 (strong signal) │
│ * descMatches × 1 (weak signal) │
│ * exact match bonus +10 │
└────────────┬────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Results with Feedback │
│ - Matched skills array │
│ - Sorted by relevance score │
│ - User-friendly feedback message │
│ "Searching for: **git commit** | │
│ Found 3 matches" │
└────────────┬────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Format & Return (index.ts) │
│ - Convert to XML via jsonToXml() │
│ - Inject into message context │
│ - Return to user │
└─────────────────────────────────────────┘
┌──────────────────────────────────┐
│ User: skill_use("git-commits") │
└────────────┬─────────────────────┘
↓
┌──────────────────────────────────┐
│ SkillUser Tool (tools/SkillUser) │
│ - Await ready state │
│ - Validate skill names │
└────────────┬─────────────────────┘
↓
┌──────────────────────────────────┐
│ Registry.controller.get(key) │
│ - Look up skill in Map │
│ - Return Skill object with: │
│ * Name, description │
│ * Content (markdown) │
│ * Resource maps (indexed) │
└────────────┬─────────────────────┘
↓
┌──────────────────────────────────┐
│ Format Skill for Injection │
│ - Skill metadata │
│ - Resource inventory (with MIME) │
│ - Full content as markdown │
│ - Base directory context │
└────────────┬─────────────────────┘
↓
┌──────────────────────────────────┐
│ Silent Injection Pattern │
│ - Inject as user message │
│ - Persists in conversation │
│ - Returns success summary │
│ { loaded: [...], notFound: []}│
└──────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ User: skill_resource("git-commits", │
│ "scripts/commit.sh") │
└──────────────┬───────────────────────────────┘
↓
┌──────────────────────────────────────────────┐
│ SkillResourceReader Tool │
│ (tools/SkillResourceReader.ts) │
│ - Validate skill_name │
│ - Parse path: "scripts/commit.sh" │
│ ├─ type = "scripts" │
│ └─ relative_path = "commit.sh" │
│ - Assert type is valid (scripts|assets|refs)│
└──────────────┬───────────────────────────────┘
↓
┌──────────────────────────────────────────────┐
│ SkillResourceResolver │
│ - Fetch skill object from registry │
│ - Look up in skill.scripts Map │
│ (pre-indexed at parse time) │
│ - Retrieve: { absolutePath, mimeType } │
│ │
│ ★ SECURITY: Only pre-indexed paths exist │
│ No way to request ../../../etc/passwd │
└──────────────┬───────────────────────────────┘
↓
┌──────────────────────────────────────────────┐
│ File I/O (SkillFs abstraction) │
│ - Read file content from absolutePath │
│ - Detect MIME type (e.g., text/plain) │
└──────────────┬───────────────────────────────┘
↓
┌──────────────────────────────────────────────┐
│ Return Injection Object │
│ { │
│ skill_name, │
│ resource_path, │
│ resource_mimetype, │
│ content │
│ } │
└──────────────┬───────────────────────────────┘
↓
┌──────────────────────────────────────────────┐
│ Silent Injection │
│ - Inject content into message │
│ - User sees resource inline │
└──────────────────────────────────────────────┘
| Decision | Why | Impact |
|---|---|---|
| ReadyStateMachine | Ensures tools don't race with async registry setup | Tools are guaranteed registry is ready before execution |
| Pre-indexed Resources | Prevents path traversal attacks | Security: only pre-registered paths retrievable |
| Factory Pattern (api.ts) | Centralizes initialization | Easy to test, swap implementations, mock components |
| Removed SkillProvider | Eliminated unnecessary abstraction | Simpler code, direct registry access, easier debugging |
| XML Serialization | Human-readable message injection | Results display nicely formatted in chat context |
| Two-phase Init | Async discovery doesn't block tool registration | Tools available immediately, discovery happens in background |
src/services/SkillRegistry.ts)initialise(): Async discovery and parsing pipelineregister(): Parse and store skill filesparseSkill(): Extract metadata and resource pathssrc/services/SkillSearcher.ts)(nameMatches × 3) + (descMatches × 1) + exactBonussrc/tools/SkillFinder.ts)src/tools/SkillUser.ts)src/tools/SkillResourceReader.ts)Configuration Priority (Last Wins):
1. Global: ~/.opencode/skills/ (lowest)
2. Project: ./.opencode/skills/ (highest)
Why This Order:
- Users install global skills once
- Projects override with local versions
- Same skill name in both → project version wins
Contributions are welcome! When adding features, follow these principles:
Skills follow the Anthropic Agent Skills Specification. Each skill is a directory containing a SKILL.md file:
skills/
my-skill/
SKILL.md
resources/
template.md
my-skill/
SKILL.md # Required: Skill metadata and instructions
references/ # Optional: Documentation and guides
guide.md
examples.md
assets/ # Optional: Binary files and templates
template.html
logo.png
scripts/ # Optional: Executable scripts
setup.sh
generate.py
---
name: my-skill
description: A brief description of what this skill does (min 20 chars)
---
# My Skill
Instructions for the AI agent when this skill is loaded...
Requirements:
name must match the directory name (lowercase, alphanumeric, hyphens only)description must be at least 20 characters and should be concise (under 500 characters recommended)
name must match exactlySkill resources are automatically discovered and categorized:
references/ directory): Documentation, guides, and reference materials
skill_resource for reading documentationassets/ directory): Templates, images, and binary files
scripts/ directory): Executable scripts that perform actions
skill_resource for reading as filesallowed-tools are informational (enforced at agent level)Contributions are welcome! Please file issues or submit pull requests on the GitHub repository.
MIT License. See the LICENSE file for details.
TypeScript
95.9%
Shell
2.6%
An interpretation of the Anthropic Agent Skills Specification for OpenCode, providing lazy-loaded skill discovery and injection.
Differentiators include:
skill_find words, words words to discover skillsskill_use "skill_name" and,skill_resource skill_relative/resource/path to read reference materialGet up and running with three common tasks:
skill_find "git commit"
Searches for skills related to writing git commits. Returns matching skills sorted by relevance.
skill_use "experts_writing_git_commits"
Loads the skill into your chat context. The AI agent can now reference it when giving advice.
skill_resource skill_name="experts_writing_git_commits" relative_path="references/guide.md"
Access specific documentation or templates from a skill without loading the entire skill.
Next steps: See Usage Examples for real-world scenarios, or jump to Plugin Tools for detailed documentation.
Create or edit your OpenCode configuration file (typically ~/.config/opencode/config.json):
{
"plugins": ["@zenobius/opencode-skillful"]
}
This plugin takes a different approach than OpenCode's built-in skills implementation:
| Aspect | Built-in OpenCode | opencode-skillful |
|---|---|---|
| Skill Loading | All skills pre-loaded into context by default | Skills loaded on-demand only |
| Memory Overhead | All skills consume tokens in every conversation | Only loaded skills consume tokens |
| Format Configuration | Fixed format (usually markdown) | Per-model configuration (JSON, XML, Markdown) |
| Skill Discovery | Limited, built-in set | Extensible, custom skills in any directory |
| Resource Access | Direct filesystem (less secure) | Pre-indexed resources (security-first) |
Unlike built-in skills that load automatically, opencode-skillful uses lazy loading:
Different LLM providers prefer different formats. Configure which format each model receives:
{
"promptRenderer": "xml",
"modelRenderers": {
"claude-3-5-sonnet": "xml",
"gpt-4": "json",
"gpt-4-turbo": "json",
"llama-2-70b": "md"
}
}
This allows you to optimize skill injection for each model without creating duplicate skill documentation.
Skills can include executable scripts in their scripts/ directory. When a skill is loaded with skill_use, the agent receives the full inventory of available scripts and can be instructed to run them.
How it works:
./scripts/setup.shskill_use, the agent sees all available scripts in the resource inventoryExample:
Your skill's SKILL.md includes: ./scripts/generate-changelog.sh
You ask: "Can you run the changelog generator from the build-utils skill?"
The agent:
skill_usescripts/generate-changelog.shWhy this approach?
Rather than a dedicated script execution tool, agents have full visibility into all skill resources and can intelligently decide when and how to run them based on your instructions and the task context. Scripts are referenced naturally in skill documentation (e.g., "Run ./scripts/setup.sh to initialize") and agents can work out the paths and execution from context.
The plugin provides three simple but powerful tools:
| Tool | Purpose | When to Use |
|---|---|---|
| skill_find | Discover skills by keyword | You want to search for relevant skills |
| skill_use | Load skills into chat | You want the AI to reference a skill |
| skill_resource | Read specific files from skills | You need a template, guide, or script from a skill |
See Plugin Tools for complete documentation.
These real-world scenarios show how to use the three tools together:
You want: The AI to help you write a commit message following best practices.
Steps:
Search for relevant skills:
skill_find "git commit"
Load the skill into your chat:
skill_use "experts_writing_git_commits"
Ask the AI: "Help me write a commit message for refactoring the auth module"
The AI now has the skill's guidance and can apply best practices to your request.
You want: Access a specific template or guide from a skill without loading the entire skill.
Steps:
Find skills in a category:
skill_find "testing"
Once you've identified a skill, read its resources:
skill_resource skill_name="testing-skill" relative_path="references/test-template.md"
This is useful when you just need a template or specific document, not full AI guidance.
You want: See what skills are available under a specific category.
Steps:
List all skills:
skill_find "*"
Filter by category:
skill_find "experts"
Search with exclusions:
skill_find "testing -performance"
This searches for testing-related skills but excludes performance testing.
* or empty: List all skillskeyword1 keyword2: AND logic (all terms must match)-term: Exclude results matching this term"exact phrase": Match exact phraseexperts, superpowers/writing: Path prefix matchingThe plugin supports multiple formats for prompt injection, allowing you to optimize results for different LLM models and use cases.
See the Configuration section for complete configuration details, including bunfig setup and global/project-level overrides.
Choose the format that works best for your LLM:
| Format | Best For | Characteristics |
|---|---|---|
| XML (default) | Claude models | Human-readable, structured, XML-optimized for Claude |
| JSON | GPT and strict parsers | Machine-readable, strict JSON structure, strong parsing support |
| Markdown | All models | Readable prose, heading-based structure, easy to read in conversations |
Set your preferences in .opencode-skillful.json:
{
"promptRenderer": "xml",
"modelRenderers": {
"claude-3-5-sonnet": "xml",
"gpt-4": "json",
"gpt-4-turbo": "json"
}
}
How It Works:
modelRenderers[modelID] is configured, that format is usedpromptRenderer default is used<Skill>
<name>git-commits</name>
<description>Guidelines for writing effective git commit messages</description>
<toolName>writing_git_commits</toolName>
</Skill>
Advantages:
{
"name": "git-commits",
"description": "Guidelines for writing effective git commit messages",
"toolName": "writing_git_commits"
}
Advantages:
# Skill
### name
- **name**: _git-commits_
### description
- **description**: _Guidelines for writing effective git commit messages_
### toolName
- **toolName**: _writing_git_commits_
Advantages:
Skills are discovered from these locations (in priority order, last wins on duplicates):
~/.config/opencode/skills/ - Standard XDG config location or (%APPDATA%/.opencode/skills for windows users).opencode/skills/ - Project-local skills (highest priority)# List all available skills
skill_find query="*"
# Search by keyword
skill_find query="git commit"
# Exclude terms
skill_find query="testing -performance"
Skills must be loaded by their fully-qualified identifier (generated from the directory path).
The skill identifier is created by converting the directory path to a slug: directory separators and hyphens become underscores.
skills/
experts/
ai/
agentic-engineer/ # Identifier: experts_ai_agentic_engineer
superpowers/
writing/
code-review/ # Identifier: superpowers_writing_code_review
When loading skills, use the full identifier:
# Load a skill by its identifier
skill_use "experts_ai_agentic_engineer"
# Load multiple skills
skill_use "experts_ai_agentic_engineer", "superpowers_writing_code_review"
# Read a reference document
skill_resource skill_name="writing-git-commits" relative_path="references/style-guide.md"
# Read a template
skill_resource skill_name="brand-guidelines" relative_path="assets/logo-usage.html"
Normally you won't need to use skill_resource directly, since the llm will do it for you.
When skill_use is called, it will be wrapped with instructions to the llm on how to load references, assets, and scripts from the skill.
But when writing your skills, there's nothing stopping you from using skill_resource to be explicit.
(Just be aware that exotic file types might need special tooling to handle them properly.)
The plugin provides three core tools implemented in src/tools/:
skill_find (SkillFinder.ts)Search for skills using natural query syntax with intelligent ranking by relevance.
Parameters:
query: Search string supporting:
* or empty: List all skillsexperts, superpowers/writing (prefix matching)git commit (multiple terms use AND logic)-term (exclude results)"exact match" (phrase matching)Returns:
skill_name: Skill identifier (e.g., experts_writing_git_commits)skill_shortname: Directory name of the skill (e.g., writing-git-commits)description: Human-readable descriptionExample Response:
<SkillSearchResults query="git commit">
<Skills>
<Skill skill_name="experts_writing_git_commits" skill_shortname="writing-git-commits">
Guidelines for writing effective git commit messages
</Skill>
</Skills>
<Summary>
<Total>42</Total>
<Matches>1</Matches>
<Feedback>Found 1 skill matching your query</Feedback>
</Summary>
</SkillSearchResults>
Then load it with:
skill_use "experts_writing_git_commits"
skill_use (SkillUser.ts)Load one or more skills into the chat context with full resource metadata.
Parameters:
skill_names: Array of skill identifiers to load (e.g., experts_ai_agentic_engineer)Features:
references: Documentation and reference filesassets: Binary files, templates, imagesscripts: Executable scripts with paths and MIME typesBehavior: Silently injects skills as user messages (persists in conversation history)
Example Response:
{ "loaded": ["experts/writing-git-commits"], "notFound": [] }
skill_resource (SkillResourceReader.ts)Read a specific resource file from a skill's directory and inject silently into the chat.
When to Use:
skill_use to access its resourcesParameters:
skill_name: The skill containing the resource (by toolName/FQDN or short name)relative_path: Path to the resource relative to the skill directoryResource Types:
Can read any of the three resource types:
references/ - Documentation, guides, and reference materialsassets/ - Templates, images, binary files, and configuration filesscripts/ - Executable scripts (shell, Python, etc.) for viewing or analysisReturns:
Behavior:
references/guide.md, assets/template.html, scripts/setup.sh)Example Responses:
Reference document:
Load Skill Resource
skill: experts/writing-git-commits
resource: references/commit-guide.md
type: text/markdown
Script file:
Load Skill Resource
skill: build-utils
resource: scripts/generate-changelog.sh
type: text/plain
Asset file:
Load Skill Resource
skill: brand-guidelines
resource: assets/logo-usage.html
type: text/html
When a search query doesn't match any skills, the response includes feedback:
<SkillSearchResults query="nonexistent">
<Skills></Skills>
<Summary>
<Total>42</Total>
<Matches>0</Matches>
<Feedback>No skills found matching your query</Feedback>
</Summary>
</SkillSearchResults>
When loading skills, if a skill name is not found, it's returned in the notFound array:
{ "loaded": ["writing-git-commits"], "notFound": ["nonexistent-skill"] }
The loaded skills are still injected into the conversation, allowing you to use available skills even if some are not found.
Resource loading failures occur when:
The tool returns an error message indicating which skill or resource path was problematic.
The plugin loads configuration from bunfig, supporting both project-local and global configuration files:
Configuration is loaded in this priority order (highest priority last):
Global config (standard platform locations):
~/.config/opencode-skillful/config.json%APPDATA%/opencode-skillful/config.jsonProject config (in your project root):
.opencode-skillful.jsonLater configuration files override earlier ones. Use project-local .opencode-skillful.json to override global settings for specific projects.
First, register the plugin in your OpenCode config (~/.config/opencode/config.json):
{
"plugins": ["@zenobius/opencode-skillful"]
}
Create .opencode-skillful.json in your project root or global config directory:
{
"debug": false,
"basePaths": ["~/.config/opencode/skills", ".opencode/skills"],
"promptRenderer": "xml",
"modelRenderers": {}
}
Configuration Fields:
debug (boolean, default: false): Enable debug output showing skill discovery stats
skill_find responses include discovered, parsed, rejected, and error countsbasePaths (array, default: standard locations): Custom skill search directories
[~/.config/opencode/skills, .opencode/skills].opencode/skills/ for project-specific skillspromptRenderer (string, default: 'xml'): Default format for prompt injection
'xml' | 'json' | 'md'modelRenderers (object, default: {}): Per-model format overrides
promptRenderer for specific models{ "gpt-4": "json", "claude-3-5-sonnet": "xml" }When any tool executes (skill_find, skill_use, skill_resource):
"anthropic-claude-3-5-sonnet")"claude-3-5-sonnet")modelRenderers configuration
modelRenderers, falls back to promptRenderer defaultExample: If your config has "claude-3-5-sonnet": "xml" and the active model is "anthropic-claude-3-5-sonnet", the plugin will:
"anthropic-claude-3-5-sonnet" (no match)"claude-3-5-sonnet" (match found! Use XML)"xml" formatThis allows different models to receive results in their preferred format without needing to specify every model variant. Configure the generic model name once and it works for all provider-prefixed variations.
~/.config/opencode-skillful/config.json:
{
"debug": false,
"promptRenderer": "xml",
"modelRenderers": {
"claude-3-5-sonnet": "xml",
"claude-3-opus": "xml",
"gpt-4": "json",
"gpt-4-turbo": "json",
"llama-2-70b": "md"
}
}
.opencode-skillful.json (project root):
{
"debug": true,
"basePaths": ["~/.config/opencode/skills", ".opencode/skills", "./vendor/skills"],
"promptRenderer": "xml",
"modelRenderers": {
"gpt-4": "json"
}
}
This project-local config:
The plugin uses a layered, modular architecture with clear separation of concerns and two-phase initialization.
┌──────────────────────────────────────────────────────┐
│ Plugin Entry Point (index.ts) │
│ - Defines 3 core tools: skill_find, skill_use, │
│ skill_resource │
│ - NOT including skill_exec (removed) │
│ - Initializes API factory and config │
│ - Manages message injection (XML serialization) │
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ API Factory Layer (api.ts, config.ts) │
│ - createApi(): initializes logger, registry, tools │
│ - getPluginConfig(): resolves base paths with proper │
│ precedence (project-local overrides global) │
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ Services Layer (src/services/) │
│ - SkillRegistry: discovery, parsing, resource │
│ mapping with ReadyStateMachine coordination │
│ - SkillSearcher: query parsing + intelligent ranking│
│ - SkillResourceResolver: safe path-based retrieval │
│ - SkillFs (via lib): filesystem abstraction (mockable)
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ Core Libraries (src/lib/) │
│ - ReadyStateMachine: async initialization sequencing │
│ - SkillFs: abstract filesystem operations │
│ - Identifiers: path ↔ tool name conversions │
│ - OpenCodeChat: message injection abstraction │
│ - xml.ts: JSON → XML serialization │
└──────────────────────────────────────────────────────┘
The plugin uses a two-phase initialization pattern to coordinate async discovery with tool execution:
PHASE 1: SYNCHRONOUS CREATION
├─ Plugin loads configuration
├─ createApi() called:
│ ├─ Creates logger
│ ├─ Creates SkillRegistry (factory, NOT initialized yet)
│ └─ Returns registry + tool creators
└─ index.ts registers tools immediately
PHASE 2: ASYNCHRONOUS DISCOVERY (Background)
├─ registry.initialise() called
│ ├─ Sets ready state to "loading"
│ ├─ Scans all base paths for SKILL.md files
│ ├─ Parses each file (YAML frontmatter + resources)
│ ├─ Pre-indexes all resources (scripts/assets/references)
│ └─ Sets ready state to "ready"
│
└─ WHY PRE-INDEXING: Prevents path traversal attacks.
Tools can only retrieve pre-registered resources,
never arbitrary filesystem paths.
PHASE 3: TOOL EXECUTION (User Requested)
├─ User calls: skill_find("git commits")
├─ Tool executes:
│ ├─ await registry.controller.ready.whenReady()
│ │ (blocks until Phase 2 completes)
│ ├─ Search registry
│ └─ Return results
└─ WHY THIS PATTERN: Multiple tools can call whenReady()
at different times without race conditions.
Simple Promise would resolve once; this allows
concurrent waiters.
┌─────────────────────────────────────────┐
│ User Query: skill_find("git commit") │
└────────────┬────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ SkillFinder Tool (tools/SkillFinder.ts) │
│ - Validates query string │
│ - Awaits: controller.ready.whenReady() │
└────────────┬────────────────────────────┘
↓ (continues when registry ready)
┌─────────────────────────────────────────┐
│ SkillSearcher.search() │
│ - Parse query via search-string library │
│ ("git commit" → include: [git,commit])│
│ - Filter ALL include terms must match │
│ (name, description, or toolName) │
│ - Exclude results matching exclude │
│ terms │
│ - Rank results: │
│ * nameMatches × 3 (strong signal) │
│ * descMatches × 1 (weak signal) │
│ * exact match bonus +10 │
└────────────┬────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Results with Feedback │
│ - Matched skills array │
│ - Sorted by relevance score │
│ - User-friendly feedback message │
│ "Searching for: **git commit** | │
│ Found 3 matches" │
└────────────┬────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Format & Return (index.ts) │
│ - Convert to XML via jsonToXml() │
│ - Inject into message context │
│ - Return to user │
└─────────────────────────────────────────┘
┌──────────────────────────────────┐
│ User: skill_use("git-commits") │
└────────────┬─────────────────────┘
↓
┌──────────────────────────────────┐
│ SkillUser Tool (tools/SkillUser) │
│ - Await ready state │
│ - Validate skill names │
└────────────┬─────────────────────┘
↓
┌──────────────────────────────────┐
│ Registry.controller.get(key) │
│ - Look up skill in Map │
│ - Return Skill object with: │
│ * Name, description │
│ * Content (markdown) │
│ * Resource maps (indexed) │
└────────────┬─────────────────────┘
↓
┌──────────────────────────────────┐
│ Format Skill for Injection │
│ - Skill metadata │
│ - Resource inventory (with MIME) │
│ - Full content as markdown │
│ - Base directory context │
└────────────┬─────────────────────┘
↓
┌──────────────────────────────────┐
│ Silent Injection Pattern │
│ - Inject as user message │
│ - Persists in conversation │
│ - Returns success summary │
│ { loaded: [...], notFound: []}│
└──────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ User: skill_resource("git-commits", │
│ "scripts/commit.sh") │
└──────────────┬───────────────────────────────┘
↓
┌──────────────────────────────────────────────┐
│ SkillResourceReader Tool │
│ (tools/SkillResourceReader.ts) │
│ - Validate skill_name │
│ - Parse path: "scripts/commit.sh" │
│ ├─ type = "scripts" │
│ └─ relative_path = "commit.sh" │
│ - Assert type is valid (scripts|assets|refs)│
└──────────────┬───────────────────────────────┘
↓
┌──────────────────────────────────────────────┐
│ SkillResourceResolver │
│ - Fetch skill object from registry │
│ - Look up in skill.scripts Map │
│ (pre-indexed at parse time) │
│ - Retrieve: { absolutePath, mimeType } │
│ │
│ ★ SECURITY: Only pre-indexed paths exist │
│ No way to request ../../../etc/passwd │
└──────────────┬───────────────────────────────┘
↓
┌──────────────────────────────────────────────┐
│ File I/O (SkillFs abstraction) │
│ - Read file content from absolutePath │
│ - Detect MIME type (e.g., text/plain) │
└──────────────┬───────────────────────────────┘
↓
┌──────────────────────────────────────────────┐
│ Return Injection Object │
│ { │
│ skill_name, │
│ resource_path, │
│ resource_mimetype, │
│ content │
│ } │
└──────────────┬───────────────────────────────┘
↓
┌──────────────────────────────────────────────┐
│ Silent Injection │
│ - Inject content into message │
│ - User sees resource inline │
└──────────────────────────────────────────────┘
| Decision | Why | Impact |
|---|---|---|
| ReadyStateMachine | Ensures tools don't race with async registry setup | Tools are guaranteed registry is ready before execution |
| Pre-indexed Resources | Prevents path traversal attacks | Security: only pre-registered paths retrievable |
| Factory Pattern (api.ts) | Centralizes initialization | Easy to test, swap implementations, mock components |
| Removed SkillProvider | Eliminated unnecessary abstraction | Simpler code, direct registry access, easier debugging |
| XML Serialization | Human-readable message injection | Results display nicely formatted in chat context |
| Two-phase Init | Async discovery doesn't block tool registration | Tools available immediately, discovery happens in background |
src/services/SkillRegistry.ts)initialise(): Async discovery and parsing pipelineregister(): Parse and store skill filesparseSkill(): Extract metadata and resource pathssrc/services/SkillSearcher.ts)(nameMatches × 3) + (descMatches × 1) + exactBonussrc/tools/SkillFinder.ts)src/tools/SkillUser.ts)src/tools/SkillResourceReader.ts)Configuration Priority (Last Wins):
1. Global: ~/.opencode/skills/ (lowest)
2. Project: ./.opencode/skills/ (highest)
Why This Order:
- Users install global skills once
- Projects override with local versions
- Same skill name in both → project version wins
Contributions are welcome! When adding features, follow these principles:
Skills follow the Anthropic Agent Skills Specification. Each skill is a directory containing a SKILL.md file:
skills/
my-skill/
SKILL.md
resources/
template.md
my-skill/
SKILL.md # Required: Skill metadata and instructions
references/ # Optional: Documentation and guides
guide.md
examples.md
assets/ # Optional: Binary files and templates
template.html
logo.png
scripts/ # Optional: Executable scripts
setup.sh
generate.py
---
name: my-skill
description: A brief description of what this skill does (min 20 chars)
---
# My Skill
Instructions for the AI agent when this skill is loaded...
Requirements:
name must match the directory name (lowercase, alphanumeric, hyphens only)description must be at least 20 characters and should be concise (under 500 characters recommended)
name must match exactlySkill resources are automatically discovered and categorized:
references/ directory): Documentation, guides, and reference materials
skill_resource for reading documentationassets/ directory): Templates, images, and binary files
scripts/ directory): Executable scripts that perform actions
skill_resource for reading as filesallowed-tools are informational (enforced at agent level)Contributions are welcome! Please file issues or submit pull requests on the GitHub repository.
MIT License. See the LICENSE file for details.
TypeScript
95.9%
Shell
2.6%