Conversation
Add documentation for combining local and remote skills from OpenHands extensions repository. Includes: - Use case explanation - Loading pattern comparison - Full example code reference - Local skill creation guide - Precedence rules Co-authored-by: openhands <openhands@all-hands.dev>
enyst
left a comment
There was a problem hiding this comment.
Reviewed latest changes. I left one inline suggestion to clarify that OpenHands uses the Claude Code plugin marketplace schema as a filter list for public skills, while local skills are merged separately. Otherwise LGTM.
Co-authored-by: openhands <openhands@all-hands.dev>
|
OpenHands-GPT-5.2 here. I pushed a docs update clarifying that |
The example was updated to use skills/ instead of local_skills/ to follow the standard plugin structure. Co-authored-by: openhands <openhands@all-hands.dev>
Address review feedback: Clarify that marketplace.json plugin entry names are used as a filter list for loading public skills, not defining skills directly. Local skills are merged separately from local_skills/ directory. Co-authored-by: openhands <openhands@all-hands.dev>
all-hands-bot
left a comment
There was a problem hiding this comment.
Taste Rating: 🟡 Acceptable - Good content but has clarity gaps that will confuse users.
Key Issue: The guide mentions concepts (marketplace.json, Claude Code plugin schema) that are never explained or demonstrated, while the example code uses a completely different approach. This creates a disconnect between what users read and what they learn.
|
Moved to draft - Evidence needs live run The Evidence section currently shows a code snippet, but not actual output from running it. Since this is a documentation PR clarifying schema requirements, acceptable evidence would be:
Please either:
|
all-hands-bot
left a comment
There was a problem hiding this comment.
Taste Rating: 🟡 Acceptable - Good content but has clarity gaps that will confuse users.
Key Issue: The guide mentions concepts (marketplace.json, Claude Code plugin schema) that are never explained or demonstrated, while the example code uses a completely different approach. This creates a disconnect between what users read and what they learn.
| │ └── greeting-helper/ | ||
| │ └── SKILL.md | ||
| ├── main.py | ||
| └── README.md |
There was a problem hiding this comment.
🟡 Suggestion - Show What You Mention: This directory structure includes .plugin/marketplace.json, but the file content is never shown and the example code below doesn't use it.
The confusion: Users will see this structure and expect to learn how marketplace.json works, but the guide takes a completely different approach (direct API calls).
Fix options:
- Remove
.plugin/from the directory structure if not teaching marketplace-based loading - Add a brief example showing marketplace.json contents with a note "See [advanced guide] for details"
- Or add this after the directory structure:
**Note**: This guide demonstrates direct loader APIs (`load_skills_from_dir()`, `load_public_skills()`). For marketplace-based configuration loading, see [link to that guide].This sets clear expectations about what users will learn.
|
|
||
| The `.plugin/marketplace.json` file follows the Claude Code plugin marketplace schema. In OpenHands, plugin entry names are used as a filter list for which public skills to load from OpenHands/extensions, while local skills live in `local_skills/` and are merged separately. | ||
|
|
||
| The guide below starts with the simplest direct loader calls (`load_skills_from_dir()` and `load_public_skills()`) so you can see exactly what each source contributes. The example repository still includes `.plugin/marketplace.json` because that is the configuration file used for repository-managed marketplace filtering. | ||
|
|
There was a problem hiding this comment.
🟡 Suggestion - Complete the Mental Model: This explanation mentions marketplace.json filtering but doesn't show the connection to the code below. Users will wonder:
- What does marketplace.json actually contain?
- How does
load_public_skills()(line 149) know about it? - When is marketplace.json loaded?
Either:
- Add a code snippet showing marketplace.json structure (even if not used in the example)
- Or simplify to: "The example repository includes
.plugin/marketplace.jsonfor advanced marketplace-based filtering (not covered in this guide). We'll use direct loader APIs instead."
Pragmatism check: If this guide is teaching the simple approach, don't introduce concepts you won't explain. It creates cognitive overhead without payoff.
|
|
||
| # Load public skills from the OpenHands extensions repository | ||
| # This pulls from the default marketplace at OpenHands/extensions | ||
| public_skills = load_public_skills() |
There was a problem hiding this comment.
🟡 Question - Where's the Filtering?: The guide mentions marketplace.json filters public skills (line 42), but this call has no arguments. How does filtering work?
If load_public_skills() loads ALL public skills (no filtering): Say so explicitly.
If it respects marketplace.json automatically: Explain when/how the file is discovered.
If filtering requires additional parameters: Show that pattern.
| # This allows local skills to override public skills with the same name | ||
| combined_skills = list(local_skills.values()) + public_skills | ||
|
|
||
| # Remove duplicates (keep first occurrence = local takes precedence) | ||
| seen_names: set[str] = set() | ||
| unique_skills = [] | ||
| for skill in combined_skills: | ||
| if skill.name not in seen_names: | ||
| seen_names.add(skill.name) | ||
| unique_skills.append(skill) | ||
|
|
||
| print(f"\nTotal combined skills: {len(unique_skills)}") |
There was a problem hiding this comment.
🟢 Nit - Minor Simplification: This deduplication is correct but verbose. More Pythonic approach:
| # This allows local skills to override public skills with the same name | |
| combined_skills = list(local_skills.values()) + public_skills | |
| # Remove duplicates (keep first occurrence = local takes precedence) | |
| seen_names: set[str] = set() | |
| unique_skills = [] | |
| for skill in combined_skills: | |
| if skill.name not in seen_names: | |
| seen_names.add(skill.name) | |
| unique_skills.append(skill) | |
| print(f"\nTotal combined skills: {len(unique_skills)}") | |
| # Combine skills with local skills taking precedence | |
| # Use dict to deduplicate by name (preserves insertion order in Python 3.7+) | |
| combined_skills = {skill.name: skill for skill in (public_skills + list(local_skills.values()))[::-1]} | |
| unique_skills = list(combined_skills.values())[::-1] |
Or even simpler if you reverse the order:
# Local skills first, then public (local overrides public with same name)
combined_by_name = {skill.name: skill for skill in public_skills}
combined_by_name.update(local_skills) # local_skills is already a dict
unique_skills = list(combined_by_name.values())However: The current explicit loop is more readable for docs/teaching. Only change if you prioritize brevity over clarity.
Summary
Updates marketplace schema documentation to clarify that both
repoandmarketplacefields are required.Closes #374
Details
The marketplace JSON schema requires both fields:
repo: The GitHub repository URL (e.g.,https://github.com/OpenHands/extensions)marketplace: Path to marketplace JSON within the repo (e.g.,marketplaces/default.json)This clarification helps users correctly configure custom skill repositories.
Testing
Documentation change - no unit tests applicable.
CI checks:
Evidence
Verification link: View conversation
Live command output against the SDK behavior documented here:
This is the behavior the docs are clarifying: users need both a repository source and a marketplace path to load a custom curated marketplace from a skills repo.
Checklist