Skip to content

[FEATURE]: Support Multi-level Nested Skill Directory Structure #10976

Closed

Description

@fastknifes

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:

  1. Non-grouped skills continue to work: Skills in first-level directories retain original names
  2. Optional grouping: group: prefix only added when skill is in a subdirectory
  3. 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 - Add group field
  • src/features/opencode-skill-loader/loader.ts - Recursive scanning logic
  • src/features/opencode-skill-loader/async-loader.ts - Async scanning logic
  • src/tools/skill/tools.ts - Tool description updates
  • src/features/opencode-skill-loader/skill-content.ts - Content processing

References

Additional Benefits

  1. Better organization: Skills can be grouped by functional domain
  2. Name conflict resolution: Same-named skills can exist in different groups
  3. Scalability: Suitable for large projects and complex skill sets
  4. Ecosystem compatibility: Consistent with Claude Code ecosystem

Related Issue: None
Labels: enhancement, skills, compatibility
Priority: Medium

Activity

  1. added
    discussionUsed for feature requests, proposals, ideas, etc. Open discussion
    on Jan 28, 2026
  2. github-actions commented on Jan 28, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of an existing issue. Please check:

    Feel free to ignore if this doesn't address your specific case.

  3. fastknifes commented on Jan 28, 2026

    @fastknifes
    Author

    This issue might be a duplicate of an existing issue. Please check:

    Feel free to ignore if this doesn't address your specific case.

    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.

  4. kylegl commented on Mar 7, 2026

    @kylegl

    @fastknifes curious why you closed this? Did you find a solution?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

discussionUsed for feature requests, proposals, ideas, etc. Open discussion

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions