# Cowork plugin validation — root cause writeup
Date: April 18, 2026
Session: packaging `compound-engineering` plugin from <https://github.com/dlove-s/compound-engineering-plugin> as a Cowork `.plugin` file
## TL;DR
Cowork's plugin validator rejects any plugin that contains a skill (or agent) whose `description` field in frontmatter includes an angle-bracket substring that looks like an HTML tag — for example `<skill-name>`, `<tag>`, `</foo>`, `<self-closing/>`. One such string in one skill fails the entire plugin install with a generic "Plugin validation failed" banner, no specific message. The same plugin validates fine in Claude Code because Claude Code does not parse descriptions as HTML.
In our case, the sole offender across 43 skills + 50 agents was a single description in `plugins/compound-engineering/skills/ce-release-notes/SKILL.md` containing `<skill-name>`. Replacing it with `some-skill` (or backtick-wrapping it) makes the plugin install cleanly.
## Symptom
In the Cowork plugin preview card: yellow banner reading "Plugin validation failed". No detail in the banner. No failing HTTP request in the HAR log (validation is entirely client-side, over Electron IPC). The Issues panel in devtools showed only unrelated Chromium deprecation warnings. The Console showed some unrelated IPC errors (BuddyBleTransport, missing Opus 4.7 model config) but no structured validation error.
In short: the failure leaves essentially no breadcrumbs in any of the obvious places (banner, network, console, issues).
## Why it fails
The Cowork preview pipeline appears to render skill/agent descriptions as markdown+inline-HTML. Chromium's HTML parser encounters `<skill-name>` and treats it as an unknown custom element. Somewhere upstream, a stricter check rejects the frontmatter (presumably schema validation that disallows raw HTML-like tokens in descriptions, or sanitization that emits an error the validator surfaces as a hard fail).
This is speculation on the exact internal check — we never saw the structured error. But the empirical behavior is clean: a single `<skill-name>` in one description → whole plugin fails; remove the angle brackets → plugin installs.
## How we found it (bisect)
We had no usable error message, so we bisected.
1. `v1` — minimal plugin (manifest + one trivial skill): PASSED
2. `v2` — all 43 skills (no agents, no extras): FAILED
3. `v4` — all 43 skills with frontmatter stripped to just `name` + `description`: FAILED (ruled out exotic fields like `argument-hint`, `disable-model-invocation`, `allowed-tools`)
4. `v5a` (first 21 skills): PASSED; `v5b` (last 22 skills): FAILED
5. `v6a` (first 11 of v5b): FAILED; `v6b` (last 11): passed
6. `v7a` (first 5 of v6a): passed; `v7b` (last 6): FAILED
7. `v8a` (first 3 of v7b): FAILED; `v8b` (last 3): passed
8. `v9a` ce-proof / `v9b` ce-release-notes / `v9c` ce-report-bug — only `ce-release-notes` FAILED in isolation
9. `v10` — ce-release-notes with `<skill-name>` replaced by `some-skill`: PASSED
Nine rounds of bisect. At every step we kept frontmatter minimal (name + description only) so we were testing content, not metadata. Once v10 passed, a regex scan of all 43 skills + 50 agents for angle-bracket-tag patterns in descriptions returned exactly one hit — the one we had already found. No other sanitization was needed.
## Things that were *not* the cause
These were ruled out by the bisect or by direct testing:
* Frontmatter fields `argument-hint`, `disable-model-invocation`, `allowed-tools`, `ce_platforms` — stripped in v4, still failed.
* Nested `agents/<category>/*.agent.md` layout vs. flat `agents/*.md` — v2 had no agents at all and still failed.
* The `.cursor-plugin/` sibling directory — excluded from every test build.
* Non-`.md` scripts embedded in skill `scripts/` subdirectories — present in many skills that passed.
* The root-level `CLAUDE.md`, `AGENTS.md`, `CHANGELOG.md`, `README.md`, `LICENSE` — v2 and v4 excluded these and still failed.
* Skill count or aggregate size — v5a (21 skills, 385 KB) passed; v6b (11 skills, 60 KB) passed. Size cap is not the issue.
## Fix, upstream
In `plugins/compound-engineering/skills/ce-release-notes/SKILL.md`, change the description:
Before:
```yaml
description: Summarize recent compound-engineering plugin releases, or answer a specific question about a past release with a version citation. Use when the user types `/ce-release-notes` or asks "what changed in compound-engineering recently?" or "what happened to <skill-name>?".
```
After (either replace with a non-bracketed placeholder, or backtick-wrap):
```yaml
description: Summarize recent compound-engineering plugin releases, or answer a specific question about a past release with a version citation. Use when the user types `/ce-release-notes` or asks "what changed in compound-engineering recently?" or "what happened to some-skill?".
```
Or:
```yaml
description: ... or "what happened to `<skill-name>`?".
```
Backtick-wrapping should work because the markdown renderer treats the content as a code span and does not try to parse it as HTML. Either fix is one-line.
## Fix, general (for any plugin author)
Before packaging a plugin for Cowork, scan every `description` field in `skills/*/SKILL.md` and `agents/**/*.md` for angle-bracket-tag-looking substrings. A simple Python regex catches the full class:
```python
import os, re, yaml
ANGLE = re.compile(r'<[/!]?[a-zA-Z][a-zA-Z0-9_\-]*[^>]*>')
def scan_plugin(root):
hits = []
for dirpath, _, files in os.walk(root):
for f in files:
if f == 'SKILL.md' or (f.endswith('.md') and '/agents/' in dirpath):
p = os.path.join(dirpath, f)
with open(p) as fh: text = fh.read()
parts = text.split('---', 2)
if len(parts) < 3: continue
try: fm = yaml.safe_load(parts[1])
except: continue
if not isinstance(fm, dict): continue
desc = fm.get('description', '')
if isinstance(desc, str):
for m in ANGLE.findall(desc):
hits.append((p, m))
return hits
```
Run that as a pre-commit hook or a CI check. Any hit is a likely Cowork install failure.
## Things Cowork itself could improve
Items worth reporting upstream so future debugging doesn't take nine bisect rounds:
1. The "Plugin validation failed" banner should include the specific rule that failed, the file/field in question, and the offending value.
2. The same structured error should be logged to the Cowork renderer console so devtools can surface it.
3. Frontmatter descriptions that may be rendered as markdown should either be escaped on display, or validated with a clear schema that disallows raw HTML-like tokens at install time with a named error.
4. A CLI validator (`claude plugin validate <path>` or equivalent) that applies the same rules the Cowork installer does would let authors catch this in their dev loop, not at install.
## Session artifacts
All intermediate `.plugin` files produced during the bisect were saved to the Cowork outputs folder — v1 through v10 plus the final working `compound-engineering.plugin` (776 KB, 298 files, all 43 skills + 50 agents). The final plugin keeps every original frontmatter field intact; only one string in one description was modified.
Proof Shared Document
Proof Shared Document
This is a collaborative document on Proof. To read or edit it programmatically:
Fetch this URL with Accept: application/json to get content + API links.
Fetch this URL with Accept: text/markdown to get raw markdown.
Snapshot endpoint: GET /api/agent/fkdtw0w2/snapshot
Edit endpoint: POST /api/agent/fkdtw0w2/edit/v2
Ops endpoint: POST /api/agent/fkdtw0w2/ops
Bug reporting: POST /api/bridge/report_bug (or /d/fkdtw0w2/bridge/report_bug)