Cowork plugin upload: SKILL.md frontmatter angle brackets silently fail validation
Summary
Cowork's .plugin bundle upload (Directory → Plugins → Personal → Add plugin) rejects bundles with Plugin validation failed whenever any SKILL.md YAML frontmatter contains literal angle brackets in a quoted string — even when:
- The bundle passes
claude plugin validatelocally. - The YAML parses correctly with every standard parser.
- The same bundle structure (sans the offending bracket pair) installs cleanly.
The rejection is silent in ~/Library/Logs/Claude/main.log (only the request shows, no response error) and in claude.ai-web.log (no MARKETPLACE_ERROR:* entry, unlike marketplace-sync failures which return per-plugin detail). The UI surfaces only Plugin validation failed with no further information.
Reproduction
- Create a minimal plugin with
name: wp-tools(or any name), one skill, and this frontmatter:
``yaml``
---
name: site-setup
description: >-
Triggers on \"add site\", \"configure site\", \"add <domain> to wp-tools\",
or any onboarding request.
---
- Zip into a
.pluginbundle and upload via Cowork's \"Add plugin\" UI. - Receive
Plugin validation failed. No local log detail.
Remove the <domain> substring (or any other <word>-style placeholder in a quoted YAML value) and the same bundle installs cleanly.
Diagnosis
Binary-searched a real wp-tools bundle (5 skills) over ~8 incremental builds. Isolated the failure to a single SKILL.md frontmatter, then to the frontmatter (not the body), then to a single character pair (< and >).
The placeholder <domain> is a common pattern when describing trigger phrases for skills (matches the convention used throughout Anthropic's own skill examples for arguments like <filename>, <path>, etc.). Plugin authors writing SKILL.md descriptions are likely to hit this.
The probable cause is a server-side validator running the frontmatter through HTML-aware sanitization that treats <word> as an unclosed tag. The fact that the rejection is silent makes the bug particularly hard to discover.
What would help
- Surface the actual server-side rejection reason in either the UI or the local upload log. The marketplace-sync path already does this (per-plugin error array); the bundle-upload path does not.
- Don't HTML-sanitize YAML string values — they are not HTML.
- At minimum, document the restriction in plugins-reference under SKILL.md frontmatter conventions.
Environment
- Claude Desktop (macOS), Cowork
- Bundle built via the documented zip-the-plugin-root convention; same build script produces other plugins that upload fine.
- Both private-repo and brand-new-name plugins exhibit the behavior; ruled out via incremental binary search.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗