Repository navigation
Description
Feature hasn't been suggested before.
- I have verified this feature I'm about to request hasn't been suggested before.
Describe the enhancement you want to request
Feature Request: Support Multi-level Nested Skill Directory Structure
Problem Statement
oh-my-opencode currently uses fs.readdir() for single-level directory scanning to discover skills, which limits how skills can be organized. In contrast, both OpenCode native and Claude Code support multi-level nested skill directory structures.
Current Behavior
Current implementation (src/features/opencode-skill-loader/loader.ts:loadSkillsFromDir):
async function loadSkillsFromDir(skillsDir: string, scope: SkillScope): Promise<LoadedSkill[]> {
const entries = await fs.readdir(skillsDir, { withFileTypes: true }).catch(() => [])
// ... only scans first-level subdirectories
}Supported directory structure:
~/.claude/skills/
├── skill1/SKILL.md ✅ Supported
├── skill2/SKILL.md ✅ Supported
└── group/ ❌ Skills in subdirectories cannot be discovered
└── skill3/SKILL.md
Expected Behavior
Support multi-level nested skill directory structures, providing group:name grouped access similar to Claude Code:
~/.claude/skills/
├── frontend/
│ ├── react/SKILL.md Access: frontend:react
│ ├── vue/SKILL.md Access: frontend:vue
│ └── ui/SKILL.md Access: frontend:ui
├── backend/
│ ├── api/SKILL.md Access: backend:api
│ └── db/SKILL.md Access: backend:db
└── deployment/
├── docker/SKILL.md Access: deployment:docker
└── k8s/SKILL.md Access: deployment:k8s
Implementation Suggestions
Approach 1: Recursive Directory Scanning (Recommended)
Reference OpenCode native implementation, use glob patterns:
import { glob } from 'glob';
async function loadSkillsFromDir(skillsDir: string, scope: SkillScope): Promise<LoadedSkill[]> {
const skillFiles = await glob('**/SKILL.md', {
cwd: skillsDir,
absolute: false,
followSymlinks: true,
});
const skills: LoadedSkill[] = [];
for (const relativePath of skillFiles) {
const skillDir = path.dirname(relativePath);
const skillName = path.basename(skillDir);
const group = path.dirname(skillDir) === '.' ? undefined : path.dirname(skillDir);
const fullPath = path.join(skillsDir, relativePath);
const resolvedPath = path.join(skillsDir, skillDir);
const skill = await loadSkillFromPath(
fullPath,
resolvedPath,
group ? `${group}:${skillName}` : skillName,
scope,
group
);
if (skill) skills.push(skill);
}
return skills;
}Approach 2: Recursive Traversal
If you don't want to introduce glob dependency:
async function loadSkillsFromDirRecursive(
dir: string,
basePath: string = '',
scope: SkillScope
): Promise<LoadedSkill[]> {
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
const skills: LoadedSkill[] = [];
for (const entry of entries) {
if (entry.name.startsWith('.')) continue;
const fullPath = path.join(dir, entry.name);
const relativePath = basePath ? path.join(basePath, entry.name) : entry.name;
if (entry.isDirectory() || entry.isSymbolicLink()) {
const resolvedPath = await resolveSymlinkAsync(fullPath);
const skillMdPath = path.join(resolvedPath, 'SKILL.md');
// Check if SKILL.md exists
try {
await fs.access(skillMdPath);
const group = basePath;
const skillName = entry.name;
const skill = await loadSkillFromPath(
skillMdPath,
resolvedPath,
group ? `${group}:${skillName}` : skillName,
scope,
group
);
if (skill) skills.push(skill);
} catch {
// If no SKILL.md, recursively scan subdirectories
const subSkills = await loadSkillsFromDirRecursive(fullPath, relativePath, scope);
skills.push(...subSkills);
}
}
}
return skills;
}Type Definition Updates
Need to add group field in src/features/opencode-skill-loader/types.ts:
export interface LoadedSkill {
name: string
group?: string // New field
path?: string
resolvedPath?: string
definition: CommandDefinition
scope: SkillScope
license?: string
compatibility?: string
metadata?: Record<string, string>
allowedTools?: string[]
mcpConfig?: SkillMcpConfig
lazyContent?: LazyContentLoader
}Tool Execution Adaptation
Update tool description and execution logic in src/tools/skill/tools.ts:
function formatSkillsXml(skills: SkillInfo[]): string {
if (skills.length === 0) return ""
const skillsXml = skills.map(skill => {
const lines = [
" <skill>",
` <name>${skill.name}</name>`,
` <description>${skill.description}</description>`,
]
if (skill.group) {
lines.push(` <group>${skill.group}</group>`)
}
if (skill.compatibility) {
lines.push(` <compatibility>${skill.compatibility}</compatibility>`)
}
lines.push(" </skill>")
return lines.join("\n")
}).join("\n")
return `\n\n<available_skills>\n${skillsXml}\n</available_skills>`
}Use Case Examples
Scenario 1: Frontend Development Skill Set
~/.claude/skills/frontend/
├── react/
│ ├── SKILL.md # React component development guide
│ ├── components.md # Reusable components reference
│ └── hooks.md # Custom Hooks documentation
├── vue/
│ ├── SKILL.md # Vue 3 development guide
│ └── composition-api.md
└── styling/
├── SKILL.md # CSS-in-JS guide
└── tailwind.md # Tailwind CSS practices
Access methods:
/frontend:react- React component development/frontend:vue- Vue 3 development/frontend:styling- CSS-in-JS
Scenario 2: Project-level Skill Grouping
In .claude/skills/:
.claude/skills/
├── testing/
│ ├── unit/SKILL.md
│ ├── integration/SKILL.md
│ └── e2e/SKILL.md
├── deployment/
│ ├── docker/SKILL.md
│ └── k8s/SKILL.md
└── documentation/
└── api/SKILL.md
Access methods:
/testing:unit- Unit testing/deployment:k8s- Kubernetes deployment
Scenario 3: OpenCode Native Compatibility
OpenCode natively supports {skill,skills}/**/SKILL.md, which means:
.opencode/skills/
├── lang/
│ ├── python/SKILL.md
│ ├── javascript/SKILL.md
│ └── typescript/SKILL.md
└── framework/
├── react/SKILL.md
└── vue/SKILL.md
Access via grouping: lang:python, framework:react
Compatibility Notes
To maintain backward compatibility:
- Non-grouped skills continue to work: Skills in first-level directories retain original names
- Optional grouping:
group:prefix only added when skill is in a subdirectory - Priority rules:
- If same-named skills exist (ungrouped vs grouped), process by load order priority
- Priority can be specified via configuration
Comparison with Claude Code
| Feature | Claude Code | oh-my-opencode (current) | Suggested |
|---|---|---|---|
| Nested directories | ✅ Supported | ❌ Not supported | ✅ Supported |
| Grouped access | group:name |
N/A | group:name |
| Recursive discovery | ✅ Auto-discovery | ❌ Single-level scan | ✅ Recursive scan |
| Compatibility | N/A | Fully compatible | Maintain compatibility |
Impact Scope
Files to modify:
src/features/opencode-skill-loader/types.ts- Addgroupfieldsrc/features/opencode-skill-loader/loader.ts- Recursive scanning logicsrc/features/opencode-skill-loader/async-loader.ts- Async scanning logicsrc/tools/skill/tools.ts- Tool description updatessrc/features/opencode-skill-loader/skill-content.ts- Content processing
References
Additional Benefits
- Better organization: Skills can be grouped by functional domain
- Name conflict resolution: Same-named skills can exist in different groups
- Scalability: Suitable for large projects and complex skill sets
- Ecosystem compatibility: Consistent with Claude Code ecosystem
Related Issue: None
Labels: enhancement, skills, compatibility
Priority: Medium
Activity
github-actions commentedon Jan 28, 2026
This issue might be a duplicate of an existing issue. Please check:
Feel free to ignore if this doesn't address your specific case.
fastknifes commentedon Jan 28, 2026
This is different from #10964.# 10964 is about multiple nested.opencode directories in workspace, and I want nested structures within the skills directory.
Not exactly the same.
kylegl commentedon Mar 7, 2026
@fastknifes curious why you closed this? Did you find a solution?